All skills
oaustegard avatar

/developing-preact

@5e58100

Specialized Preact development skill for standards-based web applications with native-first architecture and minimal dependency footprint. Use when building Preact projects, particularly those involving data visualization, interactive applications, single-page apps with HTM syntax, Web Components integration, CSV/JSON data parsing, WebGL shader visualizations, or zero-build solutions with vendored ESM imports.

Use this Skill: https://skilld.dev/gh/oaustegard/claude-skills/developing-preact

This session only. Nothing lands on disk.

assetscomponent-patterns.md

≈3.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Common Component Patterns

This file contains reusable component patterns that can be copied into projects.

Data Grid with Sorting

import { useSignal, useComputed } from '@preact/signals';
import { html } from 'htm/preact';

function DataGrid({ data, columns }) {
  const sortColumn = useSignal(null);
  const sortDirection = useSignal('asc');

  const sortedData = useComputed(() => {
    if (!sortColumn.value) return data;
    
    const sorted = [...data].sort((a, b) => {
      const aVal = a[sortColumn.value];
      const bVal = b[sortColumn.value];
      
      if (typeof aVal === 'string') {
        return aVal.localeCompare(bVal);
      }
      return aVal - bVal;
    });
    
    return sortDirection.value === 'desc' ? sorted.reverse() : sorted;
  });

  const handleSort = (column) => {
    if (sortColumn.value === column) {
      sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
    } else {
      sortColumn.value = column;
      sortDirection.value = 'asc';
    }
  };

  return html`
    <div class="overflow-x-auto">
      <table class="min-w-full bg-white border border-gray-300">
        <thead>
          <tr class="bg-gray-100">
            ${columns.map(col => html`
              <th
                key=${col.key}
                onClick=${() => handleSort(col.key)}
                class="px-4 py-2 text-left cursor-pointer hover:bg-gray-200"
              >
                ${col.label}
                ${sortColumn.value === col.key && (
                  sortDirection.value === 'asc' ? ' ↑' : ' ↓'
                )}
              </th>
            `)}
          </tr>
        </thead>
        <tbody>
          ${sortedData.value.map((row, i) => html`
            <tr key=${i} class="border-t hover:bg-gray-50">
              ${columns.map(col => html`
                <td key=${col.key} class="px-4 py-2">
                  ${row[col.key]}
                </td>
              `)}
            </tr>
          `)}
        </tbody>
      </table>
    </div>
  `;
}

// Usage:
// const columns = [
//   { key: 'name', label: 'Name' },
//   { key: 'age', label: 'Age' },
//   { key: 'email', label: 'Email' }
// ];
// html`<${DataGrid} data=${users} columns=${columns} />`;

File Upload with Drag & Drop

import { useSignal } from '@preact/signals';
import { html } from 'htm/preact';

function FileUpload({ onFileSelect, accept = '*' }) {
  const isDragging = useSignal(false);
  const files = useSignal([]);

  const handleDragOver = (e) => {
    e.preventDefault();
    isDragging.value = true;
  };

  const handleDragLeave = (e) => {
    e.preventDefault();
    isDragging.value = false;
  };

  const handleDrop = (e) => {
    e.preventDefault();
    isDragging.value = false;
    
    const droppedFiles = Array.from(e.dataTransfer.files);
    files.value = droppedFiles;
    onFileSelect?.(droppedFiles);
  };

  const handleFileInput = (e) => {
    const selectedFiles = Array.from(e.target.files);
    files.value = selectedFiles;
    onFileSelect?.(selectedFiles);
  };

  return html`
    <div
      class=${`border-2 border-dashed rounded-lg p-8 text-center transition-colors ${
        isDragging.value
          ? 'border-blue-500 bg-blue-50'
          : 'border-gray-300 bg-gray-50'
      }`}
      onDragOver=${handleDragOver}
      onDragLeave=${handleDragLeave}
      onDrop=${handleDrop}
    >
      <input
        type="file"
        onChange=${handleFileInput}
        accept=${accept}
        multiple
        class="hidden"
        id="file-input"
      />
      <label for="file-input" class="cursor-pointer">
        <div class="text-gray-600">
          <p class="text-lg font-semibold mb-2">
            Drop files here or click to browse
          </p>
          <p class="text-sm">
            ${accept === '*' ? 'Any file type' : `Accepts: ${accept}`}
          </p>
        </div>
      </label>
      ${files.value.length > 0 && html`
        <div class="mt-4">
          <p class="font-semibold mb-2">Selected files:</p>
          <ul class="text-sm text-left">
            ${files.value.map(file => html`
              <li key=${file.name} class="py-1">
                ${file.name} (${(file.size / 1024).toFixed(1)} KB)
              </li>
            `)}
          </ul>
        </div>
      `}
    </div>
  `;
}

// Usage:
// html`<${FileUpload} 
//   accept=".csv,.json" 
//   onFileSelect=${(files) => console.log(files)}
// />`;

Search with Debouncing

import { useSignal, useSignalEffect } from '@preact/signals';
import { html } from 'htm/preact';

function SearchBox({ onSearch, placeholder = 'Search...', debounceMs = 300 }) {
  const query = useSignal('');
  const results = useSignal([]);
  const loading = useSignal(false);

  useSignalEffect(() => {
    const currentQuery = query.value;
    
    if (!currentQuery) {
      results.value = [];
      return;
    }

    loading.value = true;
    const timer = setTimeout(async () => {
      try {
        const data = await onSearch(currentQuery);
        // Only update if query hasn't changed
        if (query.value === currentQuery) {
          results.value = data;
          loading.value = false;
        }
      } catch (error) {
        console.error('Search failed:', error);
        loading.value = false;
      }
    }, debounceMs);

    return () => clearTimeout(timer);
  });

  return html`
    <div class="relative">
      <input
        type="text"
        value=${query}
        onInput=${e => query.value = e.target.value}
        placeholder=${placeholder}
        class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
      />
      ${loading.value && html`
        <div class="absolute right-3 top-3">
          <div class="animate-spin h-5 w-5 border-2 border-blue-500 border-t-transparent rounded-full"></div>
        </div>
      `}
      ${results.value.length > 0 && html`
        <div class="absolute z-10 w-full mt-2 bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto">
          ${results.value.map(result => html`
            <div
              key=${result.id}
              class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
              onClick=${() => {
                query.value = result.name;
                results.value = [];
              }}
            >
              ${result.name}
            </div>
          `)}
        </div>
      `}
    </div>
  `;
}

// Usage:
// const searchUsers = async (query) => {
//   const response = await fetch(`/api/users?q=${query}`);
//   return response.json();
// };
// html`<${SearchBox} onSearch=${searchUsers} />`;

Modal Dialog

import { useEffect, useRef } from 'preact/hooks';
import { html } from 'htm/preact';

function Modal({ isOpen, onClose, title, children }) {
  const modalRef = useRef(null);
  const previousFocus = useRef(null);

  useEffect(() => {
    if (!isOpen) return;

    // Store previously focused element
    previousFocus.current = document.activeElement;

    // Focus modal
    modalRef.current?.focus();

    // Handle escape key
    const handleEscape = (e) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', handleEscape);

    // Prevent body scroll
    document.body.style.overflow = 'hidden';

    return () => {
      document.removeEventListener('keydown', handleEscape);
      document.body.style.overflow = '';
      previousFocus.current?.focus();
    };
  }, [isOpen, onClose]);

  if (!isOpen) return null;

  return html`
    <div
      class="fixed inset-0 z-50 flex items-center justify-center"
      onClick=${onClose}
    >
      <div class="fixed inset-0 bg-black bg-opacity-50"></div>
      <div
        ref=${modalRef}
        tabindex="-1"
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
        class="relative z-10 bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
        onClick=${e => e.stopPropagation()}
      >
        <div class="flex justify-between items-center mb-4">
          <h2 id="modal-title" class="text-xl font-bold">
            ${title}
          </h2>
          <button
            onClick=${onClose}
            aria-label="Close modal"
            class="text-gray-500 hover:text-gray-700"
          >
            ✕
          </button>
        </div>
        <div class="mb-4">
          ${children}
        </div>
      </div>
    </div>
  `;
}

// Usage:
// const isOpen = useSignal(false);
// html`
//   <button onClick=${() => isOpen.value = true}>Open Modal</button>
//   <${Modal}
//     isOpen=${isOpen.value}
//     onClose=${() => isOpen.value = false}
//     title="Confirm Action"
//   >
//     <p>Are you sure you want to proceed?</p>
//     <div class="flex gap-2 mt-4">
//       <button class="px-4 py-2 bg-blue-500 text-white rounded">
//         Confirm
//       </button>
//       <button 
//         onClick=${() => isOpen.value = false}
//         class="px-4 py-2 bg-gray-200 rounded"
//       >
//         Cancel
//       </button>
//     </div>
//   </${Modal}>
// `;

Tabs Component

import { useSignal } from '@preact/signals';
import { html } from 'htm/preact';

function Tabs({ tabs }) {
  const activeTab = useSignal(0);

  return html`
    <div class="w-full">
      <div class="flex border-b border-gray-300">
        ${tabs.map((tab, index) => html`
          <button
            key=${index}
            onClick=${() => activeTab.value = index}
            class=${`px-4 py-2 font-medium transition-colors ${
              activeTab.value === index
                ? 'text-blue-600 border-b-2 border-blue-600'
                : 'text-gray-600 hover:text-gray-800'
            }`}
          >
            ${tab.label}
          </button>
        `)}
      </div>
      <div class="p-4">
        ${tabs[activeTab.value]?.content}
      </div>
    </div>
  `;
}

// Usage:
// const tabs = [
//   { label: 'Profile', content: html`<div>Profile content</div>` },
//   { label: 'Settings', content: html`<div>Settings content</div>` },
//   { label: 'History', content: html`<div>History content</div>` }
// ];
// html`<${Tabs} tabs=${tabs} />`;

Toast Notifications

import { signal } from '@preact/signals';
import { html } from 'htm/preact';

// Global toast state
const toasts = signal([]);

let toastId = 0;

function addToast(message, type = 'info', duration = 3000) {
  const id = toastId++;
  toasts.value = [...toasts.value, { id, message, type }];
  
  if (duration > 0) {
    setTimeout(() => {
      removeToast(id);
    }, duration);
  }
}

function removeToast(id) {
  toasts.value = toasts.value.filter(t => t.id !== id);
}

function ToastContainer() {
  return html`
    <div class="fixed top-4 right-4 z-50 space-y-2">
      ${toasts.value.map(toast => {
        const bgColor = {
          info: 'bg-blue-500',
          success: 'bg-green-500',
          warning: 'bg-yellow-500',
          error: 'bg-red-500'
        }[toast.type] || 'bg-gray-500';

        return html`
          <div
            key=${toast.id}
            class=${`${bgColor} text-white px-6 py-3 rounded-lg shadow-lg flex items-center gap-3 animate-slideIn`}
          >
            <span>${toast.message}</span>
            <button
              onClick=${() => removeToast(toast.id)}
              class="text-white hover:text-gray-200"
            >
              ✕
            </button>
          </div>
        `;
      })}
    </div>
  `;
}

// Usage:
// Add this to your main App component:
// html`<${ToastContainer} />`
// 
// Then call from anywhere:
// addToast('Operation successful!', 'success');
// addToast('Something went wrong', 'error');

CSV Parser Component

import { useSignal } from '@preact/signals';
import { html } from 'htm/preact';

function parseCSV(text) {
  const lines = text.trim().split('\n');
  const headers = lines[0].split(',').map(h => h.trim());
  
  const data = lines.slice(1).map(line => {
    const values = line.split(',').map(v => v.trim());
    const obj = {};
    headers.forEach((header, i) => {
      obj[header] = values[i];
    });
    return obj;
  });
  
  return { headers, data };
}

function CSVUploader({ onDataParsed }) {
  const status = useSignal('');

  const handleFile = async (e) => {
    const file = e.target.files[0];
    if (!file) return;

    status.value = 'Parsing...';
    
    try {
      const text = await file.text();
      const parsed = parseCSV(text);
      onDataParsed(parsed);
      status.value = `Parsed ${parsed.data.length} rows`;
    } catch (error) {
      status.value = `Error: ${error.message}`;
    }
  };

  return html`
    <div class="space-y-4">
      <input
        type="file"
        accept=".csv"
        onChange=${handleFile}
        class="block w-full text-sm text-gray-500
          file:mr-4 file:py-2 file:px-4
          file:rounded-lg file:border-0
          file:text-sm file:font-semibold
          file:bg-blue-50 file:text-blue-700
          hover:file:bg-blue-100"
      />
      ${status.value && html`
        <p class="text-sm text-gray-600">${status}</p>
      `}
    </div>
  `;
}

// Usage:
// html`<${CSVUploader} 
//   onDataParsed=${({ headers, data }) => {
//     console.log('Headers:', headers);
//     console.log('Data:', data);
//   }}
// />`;

Source: SKILL.md on GitHub

1 warning13d4 checks · Risk SAFE
  • Gen Agent Trust Hub13d

    The skill provides specialized instructions for Preact development with a focus on 'native-first' architecture. It includes a utility script for downloading official dependencies from the NPM registry and instructions for local testing within container environments. No security issues were identified.

  • Socket13d

    No alerts

  • Snyk13d

    Risk: LOW · No issues

  • Runlayer6mo

    3/12 files flagged

Signed by skilld at 5e58100. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 weeks ago
metadata
{
  "version": "1.2.1"
}

README badge

README badge for oaustegard/claude-skills/developing-preact