Parcourir la source

cleanup and demo page

simo il y a 4 semaines
Parent
commit
f463f25fcc
61 fichiers modifiés avec 9829 ajouts et 1510 suppressions
  1. 1 1
      .astro/data-store.json
  2. 1 1
      .astro/settings.json
  3. 0 217
      README.md
  4. 0 94
      SNAP_FIX.md
  5. 0 154
      SNAP_IMPROVEMENTS.md
  6. 0 240
      SNAP_SYSTEM.md
  7. 0 120
      TESTING_SNAP.md
  8. 4 4
      astro.config.mjs
  9. 0 0
      dist/_astro/Dashboard.BYXPJRRI.js
  10. 4 0
      dist/_astro/OrbitControls.B-WH-rzu.js
  11. 0 0
      dist/_astro/Preview.RWPSjH2w.js
  12. 0 0
      dist/_astro/client.Vw0hJC5L.js
  13. 0 0
      dist/_astro/index.BiMKgvFb.css
  14. 0 0
      dist/_astro/index.astro_astro_type_script_index_0_lang.Cm5NMAxH.js
  15. 0 0
      dist/_astro/registry.C0UvCNn_.js
  16. 0 0
      dist/_astro/web.gEHESIx2.js
  17. 10 0
      dist/_routes.json
  18. 2 0
      dist/_worker.js/_@astrojs-ssr-adapter.mjs
  19. 21 0
      dist/_worker.js/_astro-internal_middleware.mjs
  20. 4 0
      dist/_worker.js/_noop-actions.mjs
  21. 13 0
      dist/_worker.js/chunks/Layout_eV-qEGCG.mjs
  22. 353 0
      dist/_worker.js/chunks/_@astro-renderers_DpxbEuk7.mjs
  23. 1129 0
      dist/_worker.js/chunks/_@astrojs-ssr-adapter_DpluC3qk.mjs
  24. 4 0
      dist/_worker.js/chunks/astro-designed-error-pages_CuMapJD2.mjs
  25. 964 0
      dist/_worker.js/chunks/astro/server_WO3f6Mge.mjs
  26. 107 0
      dist/_worker.js/chunks/cloudflare-kv-binding_DMly_2Gl.mjs
  27. 1953 0
      dist/_worker.js/chunks/image-endpoint_DVs7qPGs.mjs
  28. 3164 0
      dist/_worker.js/chunks/index_BfW_FHpJ.mjs
  29. 10 0
      dist/_worker.js/chunks/noop-middleware_C9xlyid3.mjs
  30. 106 0
      dist/_worker.js/chunks/path_CH3auf61.mjs
  31. 218 0
      dist/_worker.js/chunks/registry_B7C18axh.mjs
  32. 58 0
      dist/_worker.js/chunks/remote_BC1y8RCW.mjs
  33. 100 0
      dist/_worker.js/chunks/sharp_CG4EIo5e.mjs
  34. 68 0
      dist/_worker.js/chunks/shortlink_FrzJzDij.mjs
  35. 40 0
      dist/_worker.js/index.js
  36. 96 0
      dist/_worker.js/manifest_BP1MTNcW.mjs
  37. 3 0
      dist/_worker.js/pages/_image.astro.mjs
  38. 33 0
      dist/_worker.js/pages/_shortcode_.astro.mjs
  39. 35 0
      dist/_worker.js/pages/api/generate-link.astro.mjs
  40. 83 0
      dist/_worker.js/pages/api/weather.astro.mjs
  41. 448 0
      dist/_worker.js/pages/app.astro.mjs
  42. 87 0
      dist/_worker.js/pages/app/preview.astro.mjs
  43. 28 0
      dist/_worker.js/pages/index.astro.mjs
  44. 2 0
      dist/_worker.js/renderers.mjs
  45. 9 0
      dist/favicon.svg
  46. 235 539
      package-lock.json
  47. 4 0
      package.json
  48. 4 12
      src/components/Dashboard.tsx
  49. 0 1
      src/components/GridConfigurator.tsx
  50. 3 6
      src/components/Preview.tsx
  51. 3 30
      src/components/TabletPreview.tsx
  52. 0 1
      src/components/WidgetConfigurator.tsx
  53. 5 73
      src/components/WidgetRenderer.tsx
  54. 0 3
      src/grids/CenteredFocusGrid.tsx
  55. 0 3
      src/grids/DashboardGrid.tsx
  56. 0 6
      src/grids/SidebarContentGrid.tsx
  57. 0 3
      src/grids/ThreeColumnGrid.tsx
  58. 0 1
      src/lib/prisma.ts
  59. 1 1
      src/pages/api/weather.ts
  60. 407 0
      src/pages/demo.astro
  61. 9 0
      wrangler.jsonc

+ 1 - 1
.astro/data-store.json

@@ -1 +1 @@
-[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.3","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"entry.mjs\",\"redirects\":true,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":true,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\",\"entrypoint\":\"astro/assets/endpoint/dev\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"fs-lite\",\"options\":{\"base\":\"/home/fc/Projects/dashMaker/node_modules/.astro/sessions\"}}}"]
+[["Map",1,2],"meta::meta",["Map",3,4,5,6],"astro-version","5.14.3","astro-config-digest","{\"root\":{},\"srcDir\":{},\"publicDir\":{},\"outDir\":{},\"cacheDir\":{},\"compressHTML\":true,\"base\":\"/\",\"trailingSlash\":\"ignore\",\"output\":\"server\",\"scopedStyleStrategy\":\"attribute\",\"build\":{\"format\":\"directory\",\"client\":{},\"server\":{},\"assets\":\"_astro\",\"serverEntry\":\"index.js\",\"redirects\":false,\"inlineStylesheets\":\"auto\",\"concurrency\":1},\"server\":{\"open\":false,\"host\":true,\"port\":4321,\"streaming\":true,\"allowedHosts\":[]},\"redirects\":{},\"image\":{\"endpoint\":{\"route\":\"/_image\"},\"service\":{\"entrypoint\":\"astro/assets/services/sharp\",\"config\":{}},\"domains\":[],\"remotePatterns\":[],\"responsiveStyles\":false},\"devToolbar\":{\"enabled\":true},\"markdown\":{\"syntaxHighlight\":{\"type\":\"shiki\",\"excludeLangs\":[\"math\"]},\"shikiConfig\":{\"langs\":[],\"langAlias\":{},\"theme\":\"github-dark\",\"themes\":{},\"wrap\":false,\"transformers\":[]},\"remarkPlugins\":[],\"rehypePlugins\":[],\"remarkRehype\":{},\"gfm\":true,\"smartypants\":true},\"security\":{\"checkOrigin\":true,\"allowedDomains\":[]},\"env\":{\"schema\":{},\"validateSecrets\":false},\"experimental\":{\"clientPrerender\":false,\"contentIntellisense\":false,\"headingIdCompat\":false,\"preserveScriptOrder\":false,\"liveContentCollections\":false,\"csp\":false,\"staticImportMetaEnv\":false,\"chromeDevtoolsWorkspace\":false,\"failOnPrerenderConflict\":false},\"legacy\":{\"collections\":false},\"session\":{\"driver\":\"cloudflare-kv-binding\",\"options\":{\"binding\":\"SESSION\"}}}"]

+ 1 - 1
.astro/settings.json

@@ -1,6 +1,6 @@
 {
 	"_variables": {
-		"lastUpdateCheck": 1761182135382
+		"lastUpdateCheck": 1767394490388
 	},
 	"devToolbar": {
 		"enabled": false

+ 0 - 217
README.md

@@ -1,217 +0,0 @@
-# eInk Dashboard Builder
-
-A monochrome, monospaced, dark mode themed static site generator for eInk displays. Build custom dashboards with configurable widgets.
-
-## Features
-
-- **Monochrome Theme**: Black background, white text, optimized for eInk displays
-- **Monospaced Font**: Uses Courier New for consistent, readable display
-- **Widget System**: Modular components with standardized JSON settings
-- **Built-in Widgets**: Crypto price tracker, clock, and more
-- **Export/Import**: Save and load dashboard configurations
-
-## Getting Started
-
-### Install Dependencies
-
-```bash
-npm install
-```
-
-### Run Development Server
-
-```bash
-npm run dev
-```
-
-Visit `http://localhost:4321` to start building your dashboard.
-
-### Build Static Site
-
-```bash
-npm run build
-```
-
-The static site will be generated in the `dist/` directory, ready to deploy.
-
-## Creating Custom Widgets
-
-### 1. Define Your Widget Component
-
-Create a new file in `src/widgets/YourWidget.tsx`:
-
-```tsx
-import { Component } from 'solid-js';
-import type { WidgetSettings, WidgetSchema } from '../types/widget';
-
-interface YourWidgetProps {
-  settings: WidgetSettings;
-}
-
-export const YourWidget: Component<YourWidgetProps> = (props) => {
-  return (
-    <div style={{
-      border: '1px solid var(--border)',
-      padding: '1rem'
-    }}>
-      <div>{props.settings.title}</div>
-      {/* Your widget content */}
-    </div>
-  );
-};
-
-export const yourWidgetSchema: WidgetSchema = {
-  name: 'Your Widget',
-  description: 'Description of what your widget does',
-  settingsSchema: {
-    title: {
-      type: 'string',
-      label: 'Title',
-      default: 'My Widget',
-      required: true
-    },
-    refreshRate: {
-      type: 'number',
-      label: 'Refresh Rate (ms)',
-      default: 60000
-    },
-    enabled: {
-      type: 'boolean',
-      label: 'Enabled',
-      default: true
-    }
-  }
-};
-```
-
-### 2. Register Your Widget
-
-Add your widget to `src/widgets/registry.ts`:
-
-```tsx
-import { YourWidget, yourWidgetSchema } from './YourWidget';
-
-export const widgetRegistry: Record<string, WidgetComponent> = {
-  // ... existing widgets
-  'your-widget': {
-    schema: yourWidgetSchema,
-    Component: YourWidget
-  }
-};
-```
-
-### 3. Widget Settings Schema
-
-Each widget defines its settings using a standardized schema. Supported types:
-
-- **string**: Text input
-- **number**: Numeric input
-- **boolean**: Checkbox
-- **select**: Dropdown with predefined options
-
-Example schema:
-
-```tsx
-settingsSchema: {
-  symbol: {
-    type: 'string',
-    label: 'Crypto Symbol',
-    default: 'BTC',
-    required: true
-  },
-  interval: {
-    type: 'number',
-    label: 'Update Interval (ms)',
-    default: 60000
-  },
-  showIcon: {
-    type: 'boolean',
-    label: 'Show Icon',
-    default: true
-  },
-  theme: {
-    type: 'select',
-    label: 'Theme',
-    default: 'dark',
-    options: ['dark', 'light']
-  }
-}
-```
-
-## Built-in Widgets
-
-### Crypto Price
-
-Displays cryptocurrency prices from CoinGecko API.
-
-**Settings:**
-- `symbol`: Crypto symbol (BTC, ETH, etc.)
-- `currency`: Display currency (USD, EUR, etc.)
-- `refreshInterval`: Update frequency in milliseconds
-- `showLabel`: Show/hide label
-- `label`: Custom label text
-
-### Clock
-
-Displays current time and date.
-
-**Settings:**
-- `format`: Time format (12h/24h)
-- `showDate`: Show/hide date
-- `showLabel`: Show/hide label
-- `label`: Custom label text
-
-## Exporting Your Dashboard
-
-1. Configure your widgets in the builder
-2. Click "Export Config" to download a JSON file
-3. Deploy the static site to your eInk display
-4. Import the config later to restore your layout
-
-## Theme Customization
-
-The app uses CSS custom properties for theming. Edit `src/layouts/Layout.astro`:
-
-```css
-:root {
-  --bg: #000000;      /* Background color */
-  --fg: #ffffff;      /* Foreground/text color */
-  --border: #ffffff;  /* Border color */
-  --gray: #808080;    /* Muted text color */
-}
-```
-
-## Project Structure
-
-```
-dashMaker/
-├── src/
-│   ├── components/          # Reusable UI components
-│   │   ├── Dashboard.tsx    # Main dashboard interface
-│   │   ├── WidgetRenderer.tsx
-│   │   └── WidgetConfigurator.tsx
-│   ├── layouts/
-│   │   └── Layout.astro     # Base HTML layout with theme
-│   ├── pages/
-│   │   └── index.astro      # Main page
-│   ├── types/
-│   │   └── widget.ts        # TypeScript interfaces
-│   └── widgets/             # Widget components
-│       ├── registry.ts      # Widget registry
-│       ├── CryptoPrice.tsx
-│       └── Clock.tsx
-├── astro.config.mjs
-├── package.json
-└── tsconfig.json
-```
-
-## Technologies
-
-- **Astro**: Static site generator
-- **Solid.js**: Reactive UI components
-- **TypeScript**: Type safety
-- **CoinGecko API**: Cryptocurrency data
-
-## License
-
-ISC

+ 0 - 94
SNAP_FIX.md

@@ -1,94 +0,0 @@
-# Grid Snapping Fix
-
-## Problem
-Widgets were not snapping to grid cells despite the snap logic being implemented.
-
-## Root Cause
-The drag event handlers were expecting `data.clientX` and `data.clientY` directly from the `@neodrag/solid` library, but the library actually provides:
-
-```javascript
-{
-  offsetX: number,      // Offset relative to draggable element
-  offsetY: number,      // Offset relative to draggable element
-  event: MouseEvent,    // The actual mouse/touch event
-  rootNode: HTMLElement,
-  currentNode: HTMLElement
-}
-```
-
-The actual mouse coordinates are in `data.event.clientX` and `data.event.clientY`, not directly on the data object.
-
-## Solution
-
-### 1. Updated DragEventData Type
-```typescript
-// Before
-type DragEventData = {
-  clientX: number;
-  clientY: number;
-  offsetX?: number;
-  offsetY?: number;
-  deltaX?: number;
-  deltaY?: number;
-};
-
-// After
-type DragEventData = {
-  offsetX: number;
-  offsetY: number;
-  event: MouseEvent | TouchEvent;
-  rootNode: HTMLElement;
-  currentNode: HTMLElement;
-};
-```
-
-### 2. Updated Event Handlers
-All three drag handlers (`handleDragStart`, `handleDrag`, `handleDragEnd`) now extract clientX/clientY from the event:
-
-```typescript
-const event = data.event;
-const clientX = "clientX" in event 
-  ? event.clientX 
-  : (event as TouchEvent).touches[0].clientX;
-const clientY = "clientY" in event 
-  ? event.clientY 
-  : (event as TouchEvent).touches[0].clientY;
-```
-
-This also adds touch event support for mobile devices.
-
-## What Now Works
-
-1. ✅ Drag events properly receive mouse coordinates
-2. ✅ Snap detection calculates correct distances
-3. ✅ Visual feedback shows when hovering over cells
-4. ✅ Widgets snap to grid cells on drop
-5. ✅ Widgets resize to fit cells when snapped
-6. ✅ Touch events supported for mobile
-
-## Testing
-
-1. Start dev server: `npm run dev`
-2. Select a grid template (e.g., "Simple 2x2 Grid")
-3. Add a widget
-4. Drag the widget over a grid cell
-5. You should see:
-   - Grid cell highlights with blue glow
-   - "Drop to snap" indicator appears
-   - Console logs showing snap detection
-6. Release the widget - it should snap to the cell
-
-## Console Output (Expected)
-
-When dragging near a cell:
-```
-[Snap Debug] DRAG START { widgetId: "...", clientX: 450, clientY: 320, gridCellsCount: 4 }
-[Snap Debug] Drag start positions: { dragStartPointerPos: {...}, dragStartWidgetPos: {...} }
-[Snap Debug] Finding target: { widgetRect: {...}, widgetCenter: {...}, cellsCount: 4, threshold: 30 }
-[Snap Debug] Checking cell: { cellId: "cell-1", cellCenter: {...}, distance: 15.2, withinThreshold: true }
-[Snap Debug] Cell is candidate: { cellId: "cell-1", overlap: 0.45, score: -7.3 }
-[Snap Debug] Best cell: "cell-1"
-```
-
-## Files Modified
-- `/src/components/WidgetRenderer.tsx` - Fixed event handling in all drag handlers

+ 0 - 154
SNAP_IMPROVEMENTS.md

@@ -1,154 +0,0 @@
-# Grid Snapping Improvements - More Forgiving Algorithm
-
-## Problem
-The snapping was too strict - it would only snap when the widget was almost perfectly positioned over a cell. Users had to be very precise, making it difficult to snap widgets into cells.
-
-## Solution
-
-### 1. Increased Snap Threshold
-```typescript
-// Before
-threshold: 30  // Only 30 pixels - too strict
-
-// After  
-threshold: 500  // Much larger area - very forgiving
-```
-
-The threshold is now 500 pixels, which means the widget can be anywhere reasonably near a cell and it will consider snapping.
-
-### 2. Overlap-Based Snapping
-The algorithm now prioritizes **overlap** over distance:
-
-**Old Algorithm:**
-- Primary metric: Distance from widget center to cell center
-- Secondary metric: Overlap as a tiebreaker
-- Problem: Would snap to closest cell even with minimal overlap
-
-**New Algorithm:**
-- Primary metric: **Overlap ratio** (how much of the widget overlaps the cell)
-- Secondary metric: Distance as a tiebreaker
-- Minimum overlap required: 15% (`minOverlap: 0.15`)
-- Result: Only snaps when widget is actually **ON** the cell
-
-### 3. How It Works Now
-
-```typescript
-const SNAP_CONFIG = {
-  threshold: 500,      // Consider cells within 500px
-  minOverlap: 0.15,    // Widget must overlap cell by at least 15%
-  magneticSnap: true,  // Real-time visual feedback
-  resizeOnSnap: true,  // Auto-resize to fit cell
-};
-```
-
-**Snap Selection Logic:**
-1. Find all cells within 500px of widget center
-2. Calculate overlap ratio for each cell (0.0 to 1.0)
-3. Exclude cells with less than 15% overlap
-4. Choose cell with **highest overlap**
-5. If overlap is tied, choose the one with closer center
-
-### 4. Visual Behavior
-
-**Before:**
-- Widget near cell → Snaps immediately (annoying)
-- Widget partially on cell → Might snap to wrong cell
-- Hard to position freely
-
-**After:**
-- Widget near cell but not overlapping → No snap, free movement ✅
-- Widget partially on cell (15%+) → Highlights that cell ✅
-- Widget mostly on cell → Snaps to that cell ✅
-- Widget overlapping multiple cells → Snaps to cell with most overlap ✅
-
-## Configuration
-
-You can tune the behavior by adjusting these values in `WidgetRenderer.tsx`:
-
-```typescript
-const SNAP_CONFIG = {
-  // Larger threshold = consider more cells (current: 500px is very forgiving)
-  threshold: 500,
-  
-  // Higher minOverlap = need more widget on cell to snap
-  // 0.15 = 15% overlap required
-  // 0.25 = 25% overlap required (stricter)
-  // 0.05 = 5% overlap required (more forgiving)
-  minOverlap: 0.15,
-  
-  // Disable magnetic snapping for only-on-drop behavior
-  magneticSnap: true,
-  
-  // Disable auto-resize if you want to preserve widget size
-  resizeOnSnap: true,
-};
-```
-
-## Examples
-
-### Example 1: Small Overlap
-```
-Widget overlaps cell by 10%
-→ No snap (below 15% threshold)
-→ Widget moves freely
-```
-
-### Example 2: Good Overlap
-```
-Widget overlaps cell by 40%
-→ Cell highlights
-→ On release, snaps to cell
-→ Widget resizes to fit cell
-```
-
-### Example 3: Multiple Cells
-```
-Widget overlaps cell-1 by 20%
-Widget overlaps cell-2 by 60%
-→ cell-2 highlights (higher overlap)
-→ Snaps to cell-2
-```
-
-## Testing
-
-1. **Free Movement**: Drag widget near (but not on) a cell
-   - Expected: No highlighting, no snapping ✅
-
-2. **Partial Overlap**: Drag widget so 20% overlaps a cell
-   - Expected: Cell highlights, snaps on release ✅
-
-3. **Multiple Cells**: Drag widget over corner between two cells
-   - Expected: Only the cell with most overlap highlights ✅
-
-4. **Full Overlap**: Drag widget completely over a cell
-   - Expected: Strong highlight, definite snap ✅
-
-## Console Debug Output
-
-You'll now see more detailed logging:
-
-```javascript
-[Snap Debug] Checking cell: {
-  cellId: "cell-1",
-  distance: "245.3",
-  overlap: "0.342",          // 34.2% overlap
-  withinThreshold: true,
-  meetsMinOverlap: true      // Above 15% minimum
-}
-
-[Snap Debug] Final best cell: {
-  cellId: "cell-1",
-  overlap: "0.342"           // Chosen due to highest overlap
-}
-```
-
-## Files Modified
-- `/src/components/WidgetRenderer.tsx`:
-  - Increased `threshold` from 30 to 500
-  - Added `minOverlap` config (0.15 = 15%)
-  - Changed algorithm to prioritize overlap over distance
-  - Improved debug logging
-
----
-
-**Result**: Snapping now feels natural and forgiving. You have to actually position the widget **on** the cell for it to snap, rather than having it snap too eagerly when just passing nearby.

+ 0 - 240
SNAP_SYSTEM.md

@@ -1,240 +0,0 @@
-# Grid Snapping System - Reimplemented from Scratch
-
-## Overview
-
-The grid snapping system has been completely reimplemented with a clean, modular architecture that provides intelligent magnetic snapping for widgets to grid cells.
-
-## Architecture
-
-### Core Components
-
-1. **Snap Engine** (`WidgetRenderer.tsx`)
-   - Pure functions for geometric calculations
-   - No dependencies on React/Solid-specific code
-   - Fully testable in isolation
-
-2. **Visual Feedback** (`Dashboard.tsx`)
-   - Enhanced cell highlights during drag
-   - "Drop to snap" indicator
-   - Smooth animations and transitions
-
-## How It Works
-
-### 1. Coordinate System
-
-The system handles coordinate transformations between three spaces:
-
-- **Screen Space**: Mouse/touch coordinates from browser events
-- **Container Space**: Coordinates relative to the viewport-scaled container
-- **Grid Space**: Unscaled coordinates in the grid's coordinate system
-
-```typescript
-screenToGrid(screenX, screenY) → { x, y } in grid pixels
-```
-
-### 2. Snap Detection Algorithm
-
-The snap algorithm uses a **proximity + overlap scoring system**:
-
-```typescript
-function findSnapTarget(widgetRect, cells, threshold):
-  for each cell:
-    distance = distanceBetween(widgetCenter, cellCenter)
-    
-    if distance > threshold:
-      skip this cell
-    
-    overlap = calculateOverlapRatio(widget, cell)
-    score = distance - (overlap × 50)  // Lower is better
-    
-    if score < bestScore:
-      bestCell = cell
-  
-  return bestCell
-```
-
-**Key features:**
-- Uses center-to-center distance as primary metric
-- Overlap percentage acts as tiebreaker for close calls
-- Configurable threshold (default: 30px)
-- Only considers cells within threshold range
-
-### 3. Magnetic vs Drop Snapping
-
-The system supports two snapping modes:
-
-**Magnetic Snapping** (enabled by default):
-- Widget visually snaps during drag
-- Provides real-time feedback via cell highlighting
-- Position updated on `handleDrag` events
-
-**Drop-Only Snapping**:
-- Widget only snaps when released
-- Less visual feedback during drag
-- Position updated only on `handleDragEnd`
-
-Configure via:
-```typescript
-const SNAP_CONFIG = {
-  magneticSnap: true,  // Enable/disable magnetic snapping
-  threshold: 30,        // Distance threshold in grid pixels
-  resizeOnSnap: true,   // Auto-resize widget to fit cell
-};
-```
-
-### 4. Drag Tracking
-
-The system maintains state across the drag lifecycle:
-
-```typescript
-dragStartWidgetPos   // Widget position when drag starts (grid coords)
-dragStartPointerPos  // Pointer position when drag starts (grid coords)
-
-// During drag:
-currentOffset = currentPointerPos - dragStartPointerPos
-currentWidgetPos = dragStartWidgetPos + currentOffset
-```
-
-This approach:
-- Eliminates cumulative positioning errors
-- Handles scaled containers correctly
-- Maintains smooth dragging at any zoom level
-
-## Configuration
-
-### Snap Thresholds
-
-Adjust snap sensitivity in `WidgetRenderer.tsx`:
-
-```typescript
-const SNAP_CONFIG = {
-  threshold: 30,  // Increase for more forgiving snapping
-                  // Decrease for precise control
-};
-```
-
-### Visual Feedback
-
-Customize snap zone appearance in `Dashboard.tsx`:
-
-```typescript
-"background-color": isHovered
-  ? "rgba(59, 130, 246, 0.25)"  // Active snap target
-  : "rgba(59, 130, 246, 0.08)",  // Available snap target
-
-border: isHovered
-  ? "3px solid rgba(59, 130, 246, 0.9)"  // Solid border when hovering
-  : "2px dashed rgba(59, 130, 246, 0.3)", // Dashed border when available
-```
-
-## Key Improvements Over Previous Implementation
-
-### 1. **Cleaner Coordinate Handling**
-- ✅ Consistent coordinate space transformations
-- ✅ Single source of truth for scale calculations
-- ❌ Old: Multiple inconsistent transform calculations
-
-### 2. **Better Snap Logic**
-- ✅ Distance + overlap scoring for intelligent snapping
-- ✅ Configurable threshold
-- ❌ Old: Point-in-rect detection only
-
-### 3. **Separation of Concerns**
-- ✅ Pure functions for geometry calculations
-- ✅ Clear separation: snap logic vs. drag handling vs. visual feedback
-- ❌ Old: Tightly coupled logic
-
-### 4. **Enhanced Visual Feedback**
-- ✅ Dashed borders for available targets
-- ✅ Solid borders + glow for active target
-- ✅ "Drop to snap" indicator
-- ✅ Smooth scale animation on hover
-- ❌ Old: Simple color change
-
-### 5. **Robust Drag Tracking**
-- ✅ Tracks drag start positions in grid space
-- ✅ Calculates offsets correctly at any scale
-- ✅ No cumulative errors
-- ❌ Old: Delta-based positioning with potential drift
-
-## Usage
-
-### For Users
-
-1. **Select a grid template** from the Grid Configurator
-2. **Add widgets** using the Widget Configurator
-3. **Drag widgets** over the dashboard
-4. **Watch for visual feedback**: cells will highlight when you're close enough to snap
-5. **Release** to snap the widget into the cell
-
-### For Developers
-
-The snap system is self-contained in `WidgetRenderer.tsx`:
-
-```typescript
-// The system automatically:
-// 1. Detects when dragging near cells
-// 2. Highlights target cells (via onCellHover callback)
-// 3. Snaps position and size on drop
-// 4. Notifies parent via onSnapToCell callback
-
-<WidgetRenderer
-  config={widget}
-  gridCells={grid?.template.cells || []}
-  onPositionUpdate={handlePositionUpdate}
-  onSizeUpdate={handleSizeUpdate}
-  onSnapToCell={handleSnapToCell}
-  onCellHover={handleCellHover}
-/>
-```
-
-## Testing
-
-The snap system has been built and tested successfully:
-
-```bash
-npm run build  # ✓ Completed successfully
-```
-
-### Manual Testing Checklist
-
-- [ ] Drag widget near cell → cell highlights
-- [ ] Release widget over cell → snaps to cell position
-- [ ] Widget resizes to fit cell
-- [ ] Drag widget away from cells → no snapping
-- [ ] Scale dashboard → snapping still works correctly
-- [ ] Multiple widgets → each snaps independently
-
-## Future Enhancements
-
-Potential improvements for the snap system:
-
-1. **Multi-cell spanning**: Allow widgets to span multiple adjacent cells
-2. **Snap previews**: Show ghost outline of snapped position during drag
-3. **Keyboard modifiers**: Hold Shift to disable snapping temporarily
-4. **Snap guides**: Show alignment lines when near edges
-5. **Custom snap points**: Define additional snap points beyond cell boundaries
-6. **Snap history**: Undo/redo snap operations
-7. **Snap audio feedback**: Optional sound when snapping occurs
-
-## File Reference
-
-- **Snap Logic**: `/src/components/WidgetRenderer.tsx` (lines 25-185)
-- **Visual Feedback**: `/src/components/Dashboard.tsx` (lines 279-342)
-- **Type Definitions**: `/src/types/grid.ts`
-
-## Performance Notes
-
-The snap system is optimized for smooth 60fps dragging:
-
-- Snap calculations: O(n) where n = number of cells (typically < 20)
-- No DOM queries during drag (uses cached refs)
-- Debounced hover state updates
-- Hardware-accelerated transforms
-- Efficient overlap calculations (no iteration)
-
----
-
-**Version**: 2.0 (Reimplemented from scratch)  
-**Date**: 2025-10-10  
-**Author**: Claude Code

+ 0 - 120
TESTING_SNAP.md

@@ -1,120 +0,0 @@
-# Testing Grid Snapping
-
-## How to Test
-
-1. **Start the development server:**
-   ```bash
-   npm run dev
-   ```
-
-2. **Open the browser console** (F12) to see debug logs
-
-3. **Select a grid template:**
-   - Click on one of the grid templates (e.g., "Simple 2x2 Grid" or "Dashboard Layout")
-   - You should see the grid boxes appear with dashed borders
-
-4. **Add a widget:**
-   - Choose a widget type (Clock, Weather, etc.)
-   - Click "Add Widget"
-   - A widget will appear on the dashboard
-
-5. **Test snapping:**
-   - **Drag the widget** over one of the dashed grid boxes
-   - **Watch the console** for debug output like:
-     ```
-     [Snap Debug] Finding target: { widgetRect, widgetCenter, cellsCount, threshold }
-     [Snap Debug] Checking cell: { cellId, distance, withinThreshold }
-     [Snap Debug] Best cell: "cell-1"
-     ```
-   - **Look for visual feedback:**
-     - Grid cell should highlight when widget is near
-     - Cell border should become solid and glow
-     - "Drop to snap" indicator should appear
-
-6. **Release the widget:**
-   - Widget should snap to the cell position
-   - Widget should resize to fit the cell
-
-## What to Look For in Console
-
-### If snapping is working:
-```
-[Snap Debug] Finding target: { cellsCount: 4, threshold: 30 }
-[Snap Debug] Checking cell: { cellId: "cell-1", distance: 15.2, withinThreshold: true }
-[Snap Debug] Cell is candidate: { cellId: "cell-1", overlap: 0.45, score: -7.3 }
-[Snap Debug] Best cell: "cell-1"
-[Snap Debug] Drag: { snapped: true, targetCellId: "cell-1" }
-```
-
-### If snapping is NOT working:
-```
-[Snap Debug] Finding target: { cellsCount: 4, threshold: 30 }
-[Snap Debug] Checking cell: { cellId: "cell-1", distance: 450.8, withinThreshold: false }
-[Snap Debug] Best cell: none
-[Snap Debug] Drag: { snapped: false, targetCellId: null }
-```
-
-## Common Issues
-
-### Issue 1: Distance is too large
-**Symptom:** Console shows `distance: 450.8, withinThreshold: false`
-
-**Cause:** Coordinate space mismatch - widget and cells are in different coordinate systems
-
-**Solution:** Check that:
-- Grid cells use absolute coordinates (e.g., x: 10, y: 10)
-- Widget positions are in the same coordinate space
-- Scale transform is correctly applied
-
-### Issue 2: No cells found
-**Symptom:** Console shows `cellsCount: 0`
-
-**Cause:** Grid cells not being passed to WidgetRenderer
-
-**Solution:** Verify grid template is selected and cells are defined
-
-### Issue 3: Cells never within threshold
-**Symptom:** All cells show `withinThreshold: false`
-
-**Cause:** Threshold too small OR coordinates are scaled incorrectly
-
-**Solution:** 
-- Increase threshold in `SNAP_CONFIG`
-- Check coordinate transformation in `screenToGrid()`
-
-## Debug Commands
-
-Open browser console and run:
-
-```javascript
-// Check grid configuration
-JSON.parse(localStorage.getItem('dashboard-grid-config'))
-
-// Check widget positions
-JSON.parse(localStorage.getItem('dashboard-config'))
-
-// Clear saved data and start fresh
-localStorage.clear()
-location.reload()
-```
-
-## Expected Behavior
-
-| Action | Expected Result |
-|--------|----------------|
-| Drag widget near cell (< 30px) | Cell highlights with blue glow |
-| Drag widget over cell center | Cell shows "Drop to snap" indicator |
-| Release widget over cell | Widget snaps to cell position and size |
-| Drag widget far from cells | No highlighting, free placement |
-| Drag while no grid selected | No snapping, free placement |
-
-## Next Steps
-
-Based on console output, we can diagnose:
-
-1. **If cells are detected but distance is wrong:** Coordinate transformation bug
-2. **If cells show cellsCount: 0:** Grid cells not passing through props
-3. **If snapping works but visual feedback doesn't:** Dashboard component issue
-4. **If nothing happens:** Check if gridCells prop is empty array
-
-Please run the test and share the console output!

+ 4 - 4
astro.config.mjs

@@ -2,10 +2,10 @@ import { defineConfig } from "astro/config";
 import solidJs from "@astrojs/solid-js";
 import node from "@astrojs/node";
 
+import cloudflare from "@astrojs/cloudflare";
+
 export default defineConfig({
   integrations: [solidJs()],
   output: "server",
-  adapter: node({
-    mode: "standalone",
-  }),
-});
+  adapter: cloudflare(),
+});

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/Dashboard.BYXPJRRI.js


Fichier diff supprimé car celui-ci est trop grand
+ 4 - 0
dist/_astro/OrbitControls.B-WH-rzu.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/Preview.RWPSjH2w.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/client.Vw0hJC5L.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/index.BiMKgvFb.css


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/index.astro_astro_type_script_index_0_lang.Cm5NMAxH.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/registry.C0UvCNn_.js


Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
dist/_astro/web.gEHESIx2.js


+ 10 - 0
dist/_routes.json

@@ -0,0 +1,10 @@
+{
+  "version": 1,
+  "include": [
+    "/*"
+  ],
+  "exclude": [
+    "/_astro/*",
+    "/favicon.svg"
+  ]
+}

+ 2 - 0
dist/_worker.js/_@astrojs-ssr-adapter.mjs

@@ -0,0 +1,2 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+export { c as createExports } from './chunks/_@astrojs-ssr-adapter_DpluC3qk.mjs';

+ 21 - 0
dist/_worker.js/_astro-internal_middleware.mjs

@@ -0,0 +1,21 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import './chunks/astro-designed-error-pages_CuMapJD2.mjs';
+import './chunks/astro/server_WO3f6Mge.mjs';
+import { s as sequence } from './chunks/index_BfW_FHpJ.mjs';
+
+const onRequest$1 = (context, next) => {
+  if (context.isPrerendered) {
+    context.locals.runtime ??= {
+      env: process.env
+    };
+  }
+  return next();
+};
+
+const onRequest = sequence(
+	onRequest$1,
+	
+	
+);
+
+export { onRequest };

+ 4 - 0
dist/_worker.js/_noop-actions.mjs

@@ -0,0 +1,4 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+const server = {};
+
+export { server };

+ 13 - 0
dist/_worker.js/chunks/Layout_eV-qEGCG.mjs

@@ -0,0 +1,13 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { e as createComponent, f as createAstro, l as renderHead, o as renderSlot, r as renderTemplate } from './astro/server_WO3f6Mge.mjs';
+/* empty css                         */
+
+const $$Astro = createAstro();
+const $$Layout = createComponent(($$result, $$props, $$slots) => {
+  const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
+  Astro2.self = $$Layout;
+  const { title = "eInk Dashboard" } = Astro2.props;
+  return renderTemplate`<html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>${title}</title>${renderHead()}</head> <body> ${renderSlot($$result, $$slots["default"])} </body></html>`;
+}, "/home/fc/Projects/glance/src/layouts/Layout.astro", void 0);
+
+export { $$Layout as $ };

Fichier diff supprimé car celui-ci est trop grand
+ 353 - 0
dist/_worker.js/chunks/_@astro-renderers_DpxbEuk7.mjs


+ 1129 - 0
dist/_worker.js/chunks/_@astrojs-ssr-adapter_DpluC3qk.mjs

@@ -0,0 +1,1129 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { f as fileExtension, j as joinPaths, s as slash, p as prependForwardSlash, r as removeTrailingForwardSlash, a as appendForwardSlash, b as isInternalPath, c as collapseDuplicateTrailingSlashes, h as hasFileExtension } from './path_CH3auf61.mjs';
+import { m as matchPattern } from './remote_BC1y8RCW.mjs';
+import { r as requestIs404Or500, i as isRequestServerIsland, n as notFound, a as redirectToFallback, b as redirectToDefaultLocale, c as requestHasLocale, d as normalizeTheLocale, e as defineMiddleware, S as SERVER_ISLAND_COMPONENT, f as SERVER_ISLAND_ROUTE, g as createEndpoint, R as RouteCache, s as sequence, h as findRouteToRewrite, m as matchRoute, j as RenderContext, P as PERSIST_SYMBOL, k as getSetCookiesFromResponse } from './index_BfW_FHpJ.mjs';
+import { R as ROUTE_TYPE_HEADER, q as REROUTE_DIRECTIVE_HEADER, D as DEFAULT_404_COMPONENT, A as AstroError, v as ActionNotFoundError, w as bold, x as red, y as yellow, z as dim, B as blue, C as clientAddressSymbol, G as LocalsNotAnObject, H as REROUTABLE_STATUS_CODES, J as responseSentSymbol } from './astro/server_WO3f6Mge.mjs';
+import { D as DEFAULT_404_ROUTE, d as default404Instance, e as ensure404Route } from './astro-designed-error-pages_CuMapJD2.mjs';
+import { N as NOOP_MIDDLEWARE_FN } from './noop-middleware_C9xlyid3.mjs';
+import 'cloudflare:workers';
+
+function createI18nMiddleware(i18n, base, trailingSlash, format) {
+  if (!i18n) return (_, next) => next();
+  const payload = {
+    ...i18n,
+    trailingSlash,
+    base,
+    format};
+  const _redirectToDefaultLocale = redirectToDefaultLocale(payload);
+  const _noFoundForNonLocaleRoute = notFound(payload);
+  const _requestHasLocale = requestHasLocale(payload.locales);
+  const _redirectToFallback = redirectToFallback(payload);
+  const prefixAlways = (context, response) => {
+    const url = context.url;
+    if (url.pathname === base + "/" || url.pathname === base) {
+      return _redirectToDefaultLocale(context);
+    } else if (!_requestHasLocale(context)) {
+      return _noFoundForNonLocaleRoute(context, response);
+    }
+    return void 0;
+  };
+  const prefixOtherLocales = (context, response) => {
+    let pathnameContainsDefaultLocale = false;
+    const url = context.url;
+    for (const segment of url.pathname.split("/")) {
+      if (normalizeTheLocale(segment) === normalizeTheLocale(i18n.defaultLocale)) {
+        pathnameContainsDefaultLocale = true;
+        break;
+      }
+    }
+    if (pathnameContainsDefaultLocale) {
+      const newLocation = url.pathname.replace(`/${i18n.defaultLocale}`, "");
+      response.headers.set("Location", newLocation);
+      return _noFoundForNonLocaleRoute(context);
+    }
+    return void 0;
+  };
+  return async (context, next) => {
+    const response = await next();
+    const type = response.headers.get(ROUTE_TYPE_HEADER);
+    const isReroute = response.headers.get(REROUTE_DIRECTIVE_HEADER);
+    if (isReroute === "no" && typeof i18n.fallback === "undefined") {
+      return response;
+    }
+    if (type !== "page" && type !== "fallback") {
+      return response;
+    }
+    if (requestIs404Or500(context.request, base)) {
+      return response;
+    }
+    if (isRequestServerIsland(context.request, base)) {
+      return response;
+    }
+    const { currentLocale } = context;
+    switch (i18n.strategy) {
+      // NOTE: theoretically, we should never hit this code path
+      case "manual": {
+        return response;
+      }
+      case "domains-prefix-other-locales": {
+        if (localeHasntDomain(i18n, currentLocale)) {
+          const result = prefixOtherLocales(context, response);
+          if (result) {
+            return result;
+          }
+        }
+        break;
+      }
+      case "pathname-prefix-other-locales": {
+        const result = prefixOtherLocales(context, response);
+        if (result) {
+          return result;
+        }
+        break;
+      }
+      case "domains-prefix-always-no-redirect": {
+        if (localeHasntDomain(i18n, currentLocale)) {
+          const result = _noFoundForNonLocaleRoute(context, response);
+          if (result) {
+            return result;
+          }
+        }
+        break;
+      }
+      case "pathname-prefix-always-no-redirect": {
+        const result = _noFoundForNonLocaleRoute(context, response);
+        if (result) {
+          return result;
+        }
+        break;
+      }
+      case "pathname-prefix-always": {
+        const result = prefixAlways(context, response);
+        if (result) {
+          return result;
+        }
+        break;
+      }
+      case "domains-prefix-always": {
+        if (localeHasntDomain(i18n, currentLocale)) {
+          const result = prefixAlways(context, response);
+          if (result) {
+            return result;
+          }
+        }
+        break;
+      }
+    }
+    return _redirectToFallback(context, response);
+  };
+}
+function localeHasntDomain(i18n, currentLocale) {
+  for (const domainLocale of Object.values(i18n.domainLookupTable)) {
+    if (domainLocale === currentLocale) {
+      return false;
+    }
+  }
+  return true;
+}
+
+const NOOP_ACTIONS_MOD = {
+  server: {}
+};
+
+const FORM_CONTENT_TYPES = [
+  "application/x-www-form-urlencoded",
+  "multipart/form-data",
+  "text/plain"
+];
+const SAFE_METHODS = ["GET", "HEAD", "OPTIONS"];
+function createOriginCheckMiddleware() {
+  return defineMiddleware((context, next) => {
+    const { request, url, isPrerendered } = context;
+    if (isPrerendered) {
+      return next();
+    }
+    if (SAFE_METHODS.includes(request.method)) {
+      return next();
+    }
+    const isSameOrigin = request.headers.get("origin") === url.origin;
+    const hasContentType = request.headers.has("content-type");
+    if (hasContentType) {
+      const formLikeHeader = hasFormLikeHeader(request.headers.get("content-type"));
+      if (formLikeHeader && !isSameOrigin) {
+        return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
+          status: 403
+        });
+      }
+    } else {
+      if (!isSameOrigin) {
+        return new Response(`Cross-site ${request.method} form submissions are forbidden`, {
+          status: 403
+        });
+      }
+    }
+    return next();
+  });
+}
+function hasFormLikeHeader(contentType) {
+  if (contentType) {
+    for (const FORM_CONTENT_TYPE of FORM_CONTENT_TYPES) {
+      if (contentType.toLowerCase().includes(FORM_CONTENT_TYPE)) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+function createDefaultRoutes(manifest) {
+  const root = new URL(manifest.hrefRoot);
+  return [
+    {
+      instance: default404Instance,
+      matchesComponent: (filePath) => filePath.href === new URL(DEFAULT_404_COMPONENT, root).href,
+      route: DEFAULT_404_ROUTE.route,
+      component: DEFAULT_404_COMPONENT
+    },
+    {
+      instance: createEndpoint(manifest),
+      matchesComponent: (filePath) => filePath.href === new URL(SERVER_ISLAND_COMPONENT, root).href,
+      route: SERVER_ISLAND_ROUTE,
+      component: SERVER_ISLAND_COMPONENT
+    }
+  ];
+}
+
+class Pipeline {
+  constructor(logger, manifest, runtimeMode, renderers, resolve, serverLike, streaming, adapterName = manifest.adapterName, clientDirectives = manifest.clientDirectives, inlinedScripts = manifest.inlinedScripts, compressHTML = manifest.compressHTML, i18n = manifest.i18n, middleware = manifest.middleware, routeCache = new RouteCache(logger, runtimeMode), site = manifest.site ? new URL(manifest.site) : void 0, defaultRoutes = createDefaultRoutes(manifest), actions = manifest.actions) {
+    this.logger = logger;
+    this.manifest = manifest;
+    this.runtimeMode = runtimeMode;
+    this.renderers = renderers;
+    this.resolve = resolve;
+    this.serverLike = serverLike;
+    this.streaming = streaming;
+    this.adapterName = adapterName;
+    this.clientDirectives = clientDirectives;
+    this.inlinedScripts = inlinedScripts;
+    this.compressHTML = compressHTML;
+    this.i18n = i18n;
+    this.middleware = middleware;
+    this.routeCache = routeCache;
+    this.site = site;
+    this.defaultRoutes = defaultRoutes;
+    this.actions = actions;
+    this.internalMiddleware = [];
+    if (i18n?.strategy !== "manual") {
+      this.internalMiddleware.push(
+        createI18nMiddleware(i18n, manifest.base, manifest.trailingSlash, manifest.buildFormat)
+      );
+    }
+  }
+  internalMiddleware;
+  resolvedMiddleware = void 0;
+  resolvedActions = void 0;
+  /**
+   * Resolves the middleware from the manifest, and returns the `onRequest` function. If `onRequest` isn't there,
+   * it returns a no-op function
+   */
+  async getMiddleware() {
+    if (this.resolvedMiddleware) {
+      return this.resolvedMiddleware;
+    } else if (this.middleware) {
+      const middlewareInstance = await this.middleware();
+      const onRequest = middlewareInstance.onRequest ?? NOOP_MIDDLEWARE_FN;
+      const internalMiddlewares = [onRequest];
+      if (this.manifest.checkOrigin) {
+        internalMiddlewares.unshift(createOriginCheckMiddleware());
+      }
+      this.resolvedMiddleware = sequence(...internalMiddlewares);
+      return this.resolvedMiddleware;
+    } else {
+      this.resolvedMiddleware = NOOP_MIDDLEWARE_FN;
+      return this.resolvedMiddleware;
+    }
+  }
+  setActions(actions) {
+    this.resolvedActions = actions;
+  }
+  async getActions() {
+    if (this.resolvedActions) {
+      return this.resolvedActions;
+    } else if (this.actions) {
+      return await this.actions();
+    }
+    return NOOP_ACTIONS_MOD;
+  }
+  async getAction(path) {
+    const pathKeys = path.split(".").map((key) => decodeURIComponent(key));
+    let { server } = await this.getActions();
+    if (!server || !(typeof server === "object")) {
+      throw new TypeError(
+        `Expected \`server\` export in actions file to be an object. Received ${typeof server}.`
+      );
+    }
+    for (const key of pathKeys) {
+      if (!(key in server)) {
+        throw new AstroError({
+          ...ActionNotFoundError,
+          message: ActionNotFoundError.message(pathKeys.join("."))
+        });
+      }
+      server = server[key];
+    }
+    if (typeof server !== "function") {
+      throw new TypeError(
+        `Expected handler for action ${pathKeys.join(".")} to be a function. Received ${typeof server}.`
+      );
+    }
+    return server;
+  }
+}
+
+const RedirectComponentInstance = {
+  default() {
+    return new Response(null, {
+      status: 301
+    });
+  }
+};
+const RedirectSinglePageBuiltModule = {
+  page: () => Promise.resolve(RedirectComponentInstance),
+  onRequest: (_, next) => next(),
+  renderers: []
+};
+
+const dateTimeFormat = new Intl.DateTimeFormat([], {
+  hour: "2-digit",
+  minute: "2-digit",
+  second: "2-digit",
+  hour12: false
+});
+const levels = {
+  debug: 20,
+  info: 30,
+  warn: 40,
+  error: 50,
+  silent: 90
+};
+function log(opts, level, label, message, newLine = true) {
+  const logLevel = opts.level;
+  const dest = opts.dest;
+  const event = {
+    label,
+    level,
+    message,
+    newLine
+  };
+  if (!isLogLevelEnabled(logLevel, level)) {
+    return;
+  }
+  dest.write(event);
+}
+function isLogLevelEnabled(configuredLogLevel, level) {
+  return levels[configuredLogLevel] <= levels[level];
+}
+function info(opts, label, message, newLine = true) {
+  return log(opts, "info", label, message, newLine);
+}
+function warn(opts, label, message, newLine = true) {
+  return log(opts, "warn", label, message, newLine);
+}
+function error(opts, label, message, newLine = true) {
+  return log(opts, "error", label, message, newLine);
+}
+function debug(...args) {
+  if ("_astroGlobalDebug" in globalThis) {
+    globalThis._astroGlobalDebug(...args);
+  }
+}
+function getEventPrefix({ level, label }) {
+  const timestamp = `${dateTimeFormat.format(/* @__PURE__ */ new Date())}`;
+  const prefix = [];
+  if (level === "error" || level === "warn") {
+    prefix.push(bold(timestamp));
+    prefix.push(`[${level.toUpperCase()}]`);
+  } else {
+    prefix.push(timestamp);
+  }
+  if (label) {
+    prefix.push(`[${label}]`);
+  }
+  if (level === "error") {
+    return red(prefix.join(" "));
+  }
+  if (level === "warn") {
+    return yellow(prefix.join(" "));
+  }
+  if (prefix.length === 1) {
+    return dim(prefix[0]);
+  }
+  return dim(prefix[0]) + " " + blue(prefix.splice(1).join(" "));
+}
+class Logger {
+  options;
+  constructor(options) {
+    this.options = options;
+  }
+  info(label, message, newLine = true) {
+    info(this.options, label, message, newLine);
+  }
+  warn(label, message, newLine = true) {
+    warn(this.options, label, message, newLine);
+  }
+  error(label, message, newLine = true) {
+    error(this.options, label, message, newLine);
+  }
+  debug(label, ...messages) {
+    debug(label, ...messages);
+  }
+  level() {
+    return this.options.level;
+  }
+  forkIntegrationLogger(label) {
+    return new AstroIntegrationLogger(this.options, label);
+  }
+}
+class AstroIntegrationLogger {
+  options;
+  label;
+  constructor(logging, label) {
+    this.options = logging;
+    this.label = label;
+  }
+  /**
+   * Creates a new logger instance with a new label, but the same log options.
+   */
+  fork(label) {
+    return new AstroIntegrationLogger(this.options, label);
+  }
+  info(message) {
+    info(this.options, this.label, message);
+  }
+  warn(message) {
+    warn(this.options, this.label, message);
+  }
+  error(message) {
+    error(this.options, this.label, message);
+  }
+  debug(message) {
+    debug(this.label, message);
+  }
+}
+
+const consoleLogDestination = {
+  write(event) {
+    let dest = console.error;
+    if (levels[event.level] < levels["error"]) {
+      dest = console.info;
+    }
+    if (event.label === "SKIP_FORMAT") {
+      dest(event.message);
+    } else {
+      dest(getEventPrefix(event) + " " + event.message);
+    }
+    return true;
+  }
+};
+
+function getAssetsPrefix(fileExtension, assetsPrefix) {
+  if (!assetsPrefix) return "";
+  if (typeof assetsPrefix === "string") return assetsPrefix;
+  const dotLessFileExtension = fileExtension.slice(1);
+  if (assetsPrefix[dotLessFileExtension]) {
+    return assetsPrefix[dotLessFileExtension];
+  }
+  return assetsPrefix.fallback;
+}
+
+function createAssetLink(href, base, assetsPrefix) {
+  if (assetsPrefix) {
+    const pf = getAssetsPrefix(fileExtension(href), assetsPrefix);
+    return joinPaths(pf, slash(href));
+  } else if (base) {
+    return prependForwardSlash(joinPaths(base, slash(href)));
+  } else {
+    return href;
+  }
+}
+function createStylesheetElement(stylesheet, base, assetsPrefix) {
+  if (stylesheet.type === "inline") {
+    return {
+      props: {},
+      children: stylesheet.content
+    };
+  } else {
+    return {
+      props: {
+        rel: "stylesheet",
+        href: createAssetLink(stylesheet.src, base, assetsPrefix)
+      },
+      children: ""
+    };
+  }
+}
+function createStylesheetElementSet(stylesheets, base, assetsPrefix) {
+  return new Set(stylesheets.map((s) => createStylesheetElement(s, base, assetsPrefix)));
+}
+function createModuleScriptElement(script, base, assetsPrefix) {
+  if (script.type === "external") {
+    return createModuleScriptElementWithSrc(script.value, base, assetsPrefix);
+  } else {
+    return {
+      props: {
+        type: "module"
+      },
+      children: script.value
+    };
+  }
+}
+function createModuleScriptElementWithSrc(src, base, assetsPrefix) {
+  return {
+    props: {
+      type: "module",
+      src: createAssetLink(src, base, assetsPrefix)
+    },
+    children: ""
+  };
+}
+
+function redirectTemplate({
+  status,
+  absoluteLocation,
+  relativeLocation,
+  from
+}) {
+  const delay = status === 302 ? 2 : 0;
+  return `<!doctype html>
+<title>Redirecting to: ${relativeLocation}</title>
+<meta http-equiv="refresh" content="${delay};url=${relativeLocation}">
+<meta name="robots" content="noindex">
+<link rel="canonical" href="${absoluteLocation}">
+<body>
+	<a href="${relativeLocation}">Redirecting ${from ? `from <code>${from}</code> ` : ""}to <code>${relativeLocation}</code></a>
+</body>`;
+}
+
+class AppPipeline extends Pipeline {
+  static create({
+    logger,
+    manifest,
+    runtimeMode,
+    renderers,
+    resolve,
+    serverLike,
+    streaming,
+    defaultRoutes
+  }) {
+    const pipeline = new AppPipeline(
+      logger,
+      manifest,
+      runtimeMode,
+      renderers,
+      resolve,
+      serverLike,
+      streaming,
+      void 0,
+      void 0,
+      void 0,
+      void 0,
+      void 0,
+      void 0,
+      void 0,
+      void 0,
+      defaultRoutes
+    );
+    return pipeline;
+  }
+  headElements(routeData) {
+    const routeInfo = this.manifest.routes.find((route) => route.routeData === routeData);
+    const links = /* @__PURE__ */ new Set();
+    const scripts = /* @__PURE__ */ new Set();
+    const styles = createStylesheetElementSet(routeInfo?.styles ?? []);
+    for (const script of routeInfo?.scripts ?? []) {
+      if ("stage" in script) {
+        if (script.stage === "head-inline") {
+          scripts.add({
+            props: {},
+            children: script.children
+          });
+        }
+      } else {
+        scripts.add(createModuleScriptElement(script));
+      }
+    }
+    return { links, styles, scripts };
+  }
+  componentMetadata() {
+  }
+  async getComponentByRoute(routeData) {
+    const module = await this.getModuleForRoute(routeData);
+    return module.page();
+  }
+  async tryRewrite(payload, request) {
+    const { newUrl, pathname, routeData } = findRouteToRewrite({
+      payload,
+      request,
+      routes: this.manifest?.routes.map((r) => r.routeData),
+      trailingSlash: this.manifest.trailingSlash,
+      buildFormat: this.manifest.buildFormat,
+      base: this.manifest.base,
+      outDir: this.serverLike ? this.manifest.buildClientDir : this.manifest.outDir
+    });
+    const componentInstance = await this.getComponentByRoute(routeData);
+    return { newUrl, pathname, componentInstance, routeData };
+  }
+  async getModuleForRoute(route) {
+    for (const defaultRoute of this.defaultRoutes) {
+      if (route.component === defaultRoute.component) {
+        return {
+          page: () => Promise.resolve(defaultRoute.instance),
+          renderers: []
+        };
+      }
+    }
+    if (route.type === "redirect") {
+      return RedirectSinglePageBuiltModule;
+    } else {
+      if (this.manifest.pageMap) {
+        const importComponentInstance = this.manifest.pageMap.get(route.component);
+        if (!importComponentInstance) {
+          throw new Error(
+            `Unexpectedly unable to find a component instance for route ${route.route}`
+          );
+        }
+        return await importComponentInstance();
+      } else if (this.manifest.pageModule) {
+        return this.manifest.pageModule;
+      }
+      throw new Error(
+        "Astro couldn't find the correct page to render, probably because it wasn't correctly mapped for SSR usage. This is an internal error, please file an issue."
+      );
+    }
+  }
+}
+
+class App {
+  #manifest;
+  #manifestData;
+  #logger = new Logger({
+    dest: consoleLogDestination,
+    level: "info"
+  });
+  #baseWithoutTrailingSlash;
+  #pipeline;
+  #adapterLogger;
+  constructor(manifest, streaming = true) {
+    this.#manifest = manifest;
+    this.#manifestData = {
+      routes: manifest.routes.map((route) => route.routeData)
+    };
+    ensure404Route(this.#manifestData);
+    this.#baseWithoutTrailingSlash = removeTrailingForwardSlash(this.#manifest.base);
+    this.#pipeline = this.#createPipeline(streaming);
+    this.#adapterLogger = new AstroIntegrationLogger(
+      this.#logger.options,
+      this.#manifest.adapterName
+    );
+  }
+  getAdapterLogger() {
+    return this.#adapterLogger;
+  }
+  getAllowedDomains() {
+    return this.#manifest.allowedDomains;
+  }
+  get manifest() {
+    return this.#manifest;
+  }
+  set manifest(value) {
+    this.#manifest = value;
+  }
+  matchesAllowedDomains(forwardedHost, protocol) {
+    return App.validateForwardedHost(forwardedHost, this.#manifest.allowedDomains, protocol);
+  }
+  static validateForwardedHost(forwardedHost, allowedDomains, protocol) {
+    if (!allowedDomains || allowedDomains.length === 0) {
+      return false;
+    }
+    try {
+      const testUrl = new URL(`${protocol || "https"}://${forwardedHost}`);
+      return allowedDomains.some((pattern) => {
+        return matchPattern(testUrl, pattern);
+      });
+    } catch {
+      return false;
+    }
+  }
+  /**
+   * Creates a pipeline by reading the stored manifest
+   *
+   * @param streaming
+   * @private
+   */
+  #createPipeline(streaming = false) {
+    return AppPipeline.create({
+      logger: this.#logger,
+      manifest: this.#manifest,
+      runtimeMode: "production",
+      renderers: this.#manifest.renderers,
+      defaultRoutes: createDefaultRoutes(this.#manifest),
+      resolve: async (specifier) => {
+        if (!(specifier in this.#manifest.entryModules)) {
+          throw new Error(`Unable to resolve [${specifier}]`);
+        }
+        const bundlePath = this.#manifest.entryModules[specifier];
+        if (bundlePath.startsWith("data:") || bundlePath.length === 0) {
+          return bundlePath;
+        } else {
+          return createAssetLink(bundlePath, this.#manifest.base, this.#manifest.assetsPrefix);
+        }
+      },
+      serverLike: true,
+      streaming
+    });
+  }
+  set setManifestData(newManifestData) {
+    this.#manifestData = newManifestData;
+  }
+  removeBase(pathname) {
+    if (pathname.startsWith(this.#manifest.base)) {
+      return pathname.slice(this.#baseWithoutTrailingSlash.length + 1);
+    }
+    return pathname;
+  }
+  /**
+   * It removes the base from the request URL, prepends it with a forward slash and attempts to decoded it.
+   *
+   * If the decoding fails, it logs the error and return the pathname as is.
+   * @param request
+   * @private
+   */
+  #getPathnameFromRequest(request) {
+    const url = new URL(request.url);
+    const pathname = prependForwardSlash(this.removeBase(url.pathname));
+    try {
+      return decodeURI(pathname);
+    } catch (e) {
+      this.getAdapterLogger().error(e.toString());
+      return pathname;
+    }
+  }
+  /**
+   * Given a `Request`, it returns the `RouteData` that matches its `pathname`. By default, prerendered
+   * routes aren't returned, even if they are matched.
+   *
+   * When `allowPrerenderedRoutes` is `true`, the function returns matched prerendered routes too.
+   * @param request
+   * @param allowPrerenderedRoutes
+   */
+  match(request, allowPrerenderedRoutes = false) {
+    const url = new URL(request.url);
+    if (this.#manifest.assets.has(url.pathname)) return void 0;
+    let pathname = this.#computePathnameFromDomain(request);
+    if (!pathname) {
+      pathname = prependForwardSlash(this.removeBase(url.pathname));
+    }
+    let routeData = matchRoute(decodeURI(pathname), this.#manifestData);
+    if (!routeData) return void 0;
+    if (allowPrerenderedRoutes) {
+      return routeData;
+    } else if (routeData.prerender) {
+      return void 0;
+    }
+    return routeData;
+  }
+  #computePathnameFromDomain(request) {
+    let pathname = void 0;
+    const url = new URL(request.url);
+    if (this.#manifest.i18n && (this.#manifest.i18n.strategy === "domains-prefix-always" || this.#manifest.i18n.strategy === "domains-prefix-other-locales" || this.#manifest.i18n.strategy === "domains-prefix-always-no-redirect")) {
+      let forwardedHost = request.headers.get("X-Forwarded-Host");
+      let protocol = request.headers.get("X-Forwarded-Proto");
+      if (protocol) {
+        protocol = protocol + ":";
+      } else {
+        protocol = url.protocol;
+      }
+      if (forwardedHost && !this.matchesAllowedDomains(forwardedHost, protocol?.replace(":", ""))) {
+        forwardedHost = null;
+      }
+      let host = forwardedHost;
+      if (!host) {
+        host = request.headers.get("Host");
+      }
+      if (host && protocol) {
+        host = host.split(":")[0];
+        try {
+          let locale;
+          const hostAsUrl = new URL(`${protocol}//${host}`);
+          for (const [domainKey, localeValue] of Object.entries(
+            this.#manifest.i18n.domainLookupTable
+          )) {
+            const domainKeyAsUrl = new URL(domainKey);
+            if (hostAsUrl.host === domainKeyAsUrl.host && hostAsUrl.protocol === domainKeyAsUrl.protocol) {
+              locale = localeValue;
+              break;
+            }
+          }
+          if (locale) {
+            pathname = prependForwardSlash(
+              joinPaths(normalizeTheLocale(locale), this.removeBase(url.pathname))
+            );
+            if (url.pathname.endsWith("/")) {
+              pathname = appendForwardSlash(pathname);
+            }
+          }
+        } catch (e) {
+          this.#logger.error(
+            "router",
+            `Astro tried to parse ${protocol}//${host} as an URL, but it threw a parsing error. Check the X-Forwarded-Host and X-Forwarded-Proto headers.`
+          );
+          this.#logger.error("router", `Error: ${e}`);
+        }
+      }
+    }
+    return pathname;
+  }
+  #redirectTrailingSlash(pathname) {
+    const { trailingSlash } = this.#manifest;
+    if (pathname === "/" || isInternalPath(pathname)) {
+      return pathname;
+    }
+    const path = collapseDuplicateTrailingSlashes(pathname, trailingSlash !== "never");
+    if (path !== pathname) {
+      return path;
+    }
+    if (trailingSlash === "ignore") {
+      return pathname;
+    }
+    if (trailingSlash === "always" && !hasFileExtension(pathname)) {
+      return appendForwardSlash(pathname);
+    }
+    if (trailingSlash === "never") {
+      return removeTrailingForwardSlash(pathname);
+    }
+    return pathname;
+  }
+  async render(request, renderOptions) {
+    let routeData;
+    let locals;
+    let clientAddress;
+    let addCookieHeader;
+    const url = new URL(request.url);
+    const redirect = this.#redirectTrailingSlash(url.pathname);
+    const prerenderedErrorPageFetch = renderOptions?.prerenderedErrorPageFetch ?? fetch;
+    if (redirect !== url.pathname) {
+      const status = request.method === "GET" ? 301 : 308;
+      return new Response(
+        redirectTemplate({
+          status,
+          relativeLocation: url.pathname,
+          absoluteLocation: redirect,
+          from: request.url
+        }),
+        {
+          status,
+          headers: {
+            location: redirect + url.search
+          }
+        }
+      );
+    }
+    addCookieHeader = renderOptions?.addCookieHeader;
+    clientAddress = renderOptions?.clientAddress ?? Reflect.get(request, clientAddressSymbol);
+    routeData = renderOptions?.routeData;
+    locals = renderOptions?.locals;
+    if (routeData) {
+      this.#logger.debug(
+        "router",
+        "The adapter " + this.#manifest.adapterName + " provided a custom RouteData for ",
+        request.url
+      );
+      this.#logger.debug("router", "RouteData:\n" + routeData);
+    }
+    if (locals) {
+      if (typeof locals !== "object") {
+        const error = new AstroError(LocalsNotAnObject);
+        this.#logger.error(null, error.stack);
+        return this.#renderError(request, {
+          status: 500,
+          error,
+          clientAddress,
+          prerenderedErrorPageFetch
+        });
+      }
+    }
+    if (!routeData) {
+      routeData = this.match(request);
+      this.#logger.debug("router", "Astro matched the following route for " + request.url);
+      this.#logger.debug("router", "RouteData:\n" + routeData);
+    }
+    if (!routeData) {
+      routeData = this.#manifestData.routes.find(
+        (route) => route.component === "404.astro" || route.component === DEFAULT_404_COMPONENT
+      );
+    }
+    if (!routeData) {
+      this.#logger.debug("router", "Astro hasn't found routes that match " + request.url);
+      this.#logger.debug("router", "Here's the available routes:\n", this.#manifestData);
+      return this.#renderError(request, {
+        locals,
+        status: 404,
+        clientAddress,
+        prerenderedErrorPageFetch
+      });
+    }
+    const pathname = this.#getPathnameFromRequest(request);
+    const defaultStatus = this.#getDefaultStatusCode(routeData, pathname);
+    let response;
+    let session;
+    try {
+      const mod = await this.#pipeline.getModuleForRoute(routeData);
+      const renderContext = await RenderContext.create({
+        pipeline: this.#pipeline,
+        locals,
+        pathname,
+        request,
+        routeData,
+        status: defaultStatus,
+        clientAddress
+      });
+      session = renderContext.session;
+      response = await renderContext.render(await mod.page());
+    } catch (err) {
+      this.#logger.error(null, err.stack || err.message || String(err));
+      return this.#renderError(request, {
+        locals,
+        status: 500,
+        error: err,
+        clientAddress,
+        prerenderedErrorPageFetch
+      });
+    } finally {
+      await session?.[PERSIST_SYMBOL]();
+    }
+    if (REROUTABLE_STATUS_CODES.includes(response.status) && response.headers.get(REROUTE_DIRECTIVE_HEADER) !== "no") {
+      return this.#renderError(request, {
+        locals,
+        response,
+        status: response.status,
+        // We don't have an error to report here. Passing null means we pass nothing intentionally
+        // while undefined means there's no error
+        error: response.status === 500 ? null : void 0,
+        clientAddress,
+        prerenderedErrorPageFetch
+      });
+    }
+    if (response.headers.has(REROUTE_DIRECTIVE_HEADER)) {
+      response.headers.delete(REROUTE_DIRECTIVE_HEADER);
+    }
+    if (addCookieHeader) {
+      for (const setCookieHeaderValue of App.getSetCookieFromResponse(response)) {
+        response.headers.append("set-cookie", setCookieHeaderValue);
+      }
+    }
+    Reflect.set(response, responseSentSymbol, true);
+    return response;
+  }
+  setCookieHeaders(response) {
+    return getSetCookiesFromResponse(response);
+  }
+  /**
+   * Reads all the cookies written by `Astro.cookie.set()` onto the passed response.
+   * For example,
+   * ```ts
+   * for (const cookie_ of App.getSetCookieFromResponse(response)) {
+   *     const cookie: string = cookie_
+   * }
+   * ```
+   * @param response The response to read cookies from.
+   * @returns An iterator that yields key-value pairs as equal-sign-separated strings.
+   */
+  static getSetCookieFromResponse = getSetCookiesFromResponse;
+  /**
+   * If it is a known error code, try sending the according page (e.g. 404.astro / 500.astro).
+   * This also handles pre-rendered /404 or /500 routes
+   */
+  async #renderError(request, {
+    locals,
+    status,
+    response: originalResponse,
+    skipMiddleware = false,
+    error,
+    clientAddress,
+    prerenderedErrorPageFetch
+  }) {
+    const errorRoutePath = `/${status}${this.#manifest.trailingSlash === "always" ? "/" : ""}`;
+    const errorRouteData = matchRoute(errorRoutePath, this.#manifestData);
+    const url = new URL(request.url);
+    if (errorRouteData) {
+      if (errorRouteData.prerender) {
+        const maybeDotHtml = errorRouteData.route.endsWith(`/${status}`) ? ".html" : "";
+        const statusURL = new URL(
+          `${this.#baseWithoutTrailingSlash}/${status}${maybeDotHtml}`,
+          url
+        );
+        if (statusURL.toString() !== request.url) {
+          const response2 = await prerenderedErrorPageFetch(statusURL.toString());
+          const override = { status, removeContentEncodingHeaders: true };
+          return this.#mergeResponses(response2, originalResponse, override);
+        }
+      }
+      const mod = await this.#pipeline.getModuleForRoute(errorRouteData);
+      let session;
+      try {
+        const renderContext = await RenderContext.create({
+          locals,
+          pipeline: this.#pipeline,
+          middleware: skipMiddleware ? NOOP_MIDDLEWARE_FN : void 0,
+          pathname: this.#getPathnameFromRequest(request),
+          request,
+          routeData: errorRouteData,
+          status,
+          props: { error },
+          clientAddress
+        });
+        session = renderContext.session;
+        const response2 = await renderContext.render(await mod.page());
+        return this.#mergeResponses(response2, originalResponse);
+      } catch {
+        if (skipMiddleware === false) {
+          return this.#renderError(request, {
+            locals,
+            status,
+            response: originalResponse,
+            skipMiddleware: true,
+            clientAddress,
+            prerenderedErrorPageFetch
+          });
+        }
+      } finally {
+        await session?.[PERSIST_SYMBOL]();
+      }
+    }
+    const response = this.#mergeResponses(new Response(null, { status }), originalResponse);
+    Reflect.set(response, responseSentSymbol, true);
+    return response;
+  }
+  #mergeResponses(newResponse, originalResponse, override) {
+    let newResponseHeaders = newResponse.headers;
+    if (override?.removeContentEncodingHeaders) {
+      newResponseHeaders = new Headers(newResponseHeaders);
+      newResponseHeaders.delete("Content-Encoding");
+      newResponseHeaders.delete("Content-Length");
+    }
+    if (!originalResponse) {
+      if (override !== void 0) {
+        return new Response(newResponse.body, {
+          status: override.status,
+          statusText: newResponse.statusText,
+          headers: newResponseHeaders
+        });
+      }
+      return newResponse;
+    }
+    const status = override?.status ? override.status : originalResponse.status === 200 ? newResponse.status : originalResponse.status;
+    try {
+      originalResponse.headers.delete("Content-type");
+    } catch {
+    }
+    const mergedHeaders = new Map([
+      ...Array.from(newResponseHeaders),
+      ...Array.from(originalResponse.headers)
+    ]);
+    const newHeaders = new Headers();
+    for (const [name, value] of mergedHeaders) {
+      newHeaders.set(name, value);
+    }
+    return new Response(newResponse.body, {
+      status,
+      statusText: status === 200 ? newResponse.statusText : originalResponse.statusText,
+      // If you're looking at here for possible bugs, it means that it's not a bug.
+      // With the middleware, users can meddle with headers, and we should pass to the 404/500.
+      // If users see something weird, it's because they are setting some headers they should not.
+      //
+      // Although, we don't want it to replace the content-type, because the error page must return `text/html`
+      headers: newHeaders
+    });
+  }
+  #getDefaultStatusCode(routeData, pathname) {
+    if (!routeData.pattern.test(pathname)) {
+      for (const fallbackRoute of routeData.fallbackRoutes) {
+        if (fallbackRoute.pattern.test(pathname)) {
+          return 302;
+        }
+      }
+    }
+    const route = removeTrailingForwardSlash(routeData.route);
+    if (route.endsWith("/404")) return 404;
+    if (route.endsWith("/500")) return 500;
+    return 200;
+  }
+}
+
+async function handle(manifest, app, request, env, context) {
+  const { pathname } = new URL(request.url);
+  const bindingName = "SESSION";
+  globalThis.__env__ ??= {};
+  globalThis.__env__[bindingName] = env[bindingName];
+  if (manifest.assets.has(pathname)) {
+    return env.ASSETS.fetch(request.url.replace(/\.html$/, ""));
+  }
+  const routeData = app.match(request);
+  if (!routeData) {
+    const asset = await env.ASSETS.fetch(
+      request.url.replace(/index.html$/, "").replace(/\.html$/, "")
+    );
+    if (asset.status !== 404) {
+      return asset;
+    }
+  }
+  Reflect.set(request, Symbol.for("astro.clientAddress"), request.headers.get("cf-connecting-ip"));
+  const locals = {
+    runtime: {
+      env,
+      cf: request.cf,
+      caches,
+      ctx: {
+        waitUntil: (promise) => context.waitUntil(promise),
+        // Currently not available: https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions
+        passThroughOnException: () => {
+          throw new Error(
+            "`passThroughOnException` is currently not available in Cloudflare Pages. See https://developers.cloudflare.com/pages/platform/known-issues/#pages-functions."
+          );
+        },
+        props: {}
+      }
+    }
+  };
+  const response = await app.render(
+    request,
+    {
+      routeData,
+      locals,
+      prerenderedErrorPageFetch: async (url) => {
+        return env.ASSETS.fetch(url.replace(/\.html$/, ""));
+      }
+    }
+  );
+  if (app.setCookieHeaders) {
+    for (const setCookieHeader of app.setCookieHeaders(response)) {
+      response.headers.append("Set-Cookie", setCookieHeader);
+    }
+  }
+  return response;
+}
+
+function createExports(manifest) {
+  const app = new App(manifest);
+  const fetch = async (request, env, context) => {
+    return await handle(manifest, app, request, env, context);
+  };
+  return { default: { fetch } };
+}
+
+const serverEntrypointModule = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+  __proto__: null,
+  createExports
+}, Symbol.toStringTag, { value: 'Module' }));
+
+export { createExports as c, serverEntrypointModule as s };

Fichier diff supprimé car celui-ci est trop grand
+ 4 - 0
dist/_worker.js/chunks/astro-designed-error-pages_CuMapJD2.mjs


Fichier diff supprimé car celui-ci est trop grand
+ 964 - 0
dist/_worker.js/chunks/astro/server_WO3f6Mge.mjs


+ 107 - 0
dist/_worker.js/chunks/cloudflare-kv-binding_DMly_2Gl.mjs

@@ -0,0 +1,107 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+function defineDriver(factory) {
+  return factory;
+}
+function normalizeKey(key, sep = ":") {
+  if (!key) {
+    return "";
+  }
+  return key.replace(/[:/\\]/g, sep).replace(/^[:/\\]|[:/\\]$/g, "");
+}
+function joinKeys(...keys) {
+  return keys.map((key) => normalizeKey(key)).filter(Boolean).join(":");
+}
+function createError(driver, message, opts) {
+  const err = new Error(`[unstorage] [${driver}] ${message}`, opts);
+  if (Error.captureStackTrace) {
+    Error.captureStackTrace(err, createError);
+  }
+  return err;
+}
+
+function getBinding(binding) {
+  let bindingName = "[binding]";
+  if (typeof binding === "string") {
+    bindingName = binding;
+    binding = globalThis[bindingName] || globalThis.__env__?.[bindingName];
+  }
+  if (!binding) {
+    throw createError(
+      "cloudflare",
+      `Invalid binding \`${bindingName}\`: \`${binding}\``
+    );
+  }
+  for (const key of ["get", "put", "delete"]) {
+    if (!(key in binding)) {
+      throw createError(
+        "cloudflare",
+        `Invalid binding \`${bindingName}\`: \`${key}\` key is missing`
+      );
+    }
+  }
+  return binding;
+}
+function getKVBinding(binding = "STORAGE") {
+  return getBinding(binding);
+}
+
+const DRIVER_NAME = "cloudflare-kv-binding";
+const cloudflareKvBinding = defineDriver((opts) => {
+  const r = (key = "") => opts.base ? joinKeys(opts.base, key) : key;
+  async function getKeys(base = "") {
+    base = r(base);
+    const binding = getKVBinding(opts.binding);
+    const keys = [];
+    let cursor = void 0;
+    do {
+      const kvList = await binding.list({ prefix: base || void 0, cursor });
+      keys.push(...kvList.keys);
+      cursor = kvList.list_complete ? void 0 : kvList.cursor;
+    } while (cursor);
+    return keys.map((key) => key.name);
+  }
+  return {
+    name: DRIVER_NAME,
+    options: opts,
+    getInstance: () => getKVBinding(opts.binding),
+    async hasItem(key) {
+      key = r(key);
+      const binding = getKVBinding(opts.binding);
+      return await binding.get(key) !== null;
+    },
+    getItem(key) {
+      key = r(key);
+      const binding = getKVBinding(opts.binding);
+      return binding.get(key);
+    },
+    setItem(key, value, topts) {
+      key = r(key);
+      const binding = getKVBinding(opts.binding);
+      return binding.put(
+        key,
+        value,
+        topts ? {
+          expirationTtl: topts?.ttl ? Math.max(topts.ttl, opts.minTTL ?? 60) : void 0,
+          ...topts
+        } : void 0
+      );
+    },
+    removeItem(key) {
+      key = r(key);
+      const binding = getKVBinding(opts.binding);
+      return binding.delete(key);
+    },
+    getKeys(base) {
+      return getKeys(base).then(
+        (keys) => keys.map((key) => opts.base ? key.slice(opts.base.length) : key)
+      );
+    },
+    async clear(base) {
+      const binding = getKVBinding(opts.binding);
+      const keys = await getKeys(base);
+      await Promise.all(keys.map((key) => binding.delete(key)));
+    }
+  };
+});
+
+export { cloudflareKvBinding as default };

+ 1953 - 0
dist/_worker.js/chunks/image-endpoint_DVs7qPGs.mjs

@@ -0,0 +1,1953 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { j as joinPaths, i as isRemotePath } from './path_CH3auf61.mjs';
+import { A as AstroError, E as ExpectedImage, L as LocalImageUsedWrongly, M as MissingImageDimension, U as UnsupportedImageFormat, I as IncompatibleDescriptorOptions, a as UnsupportedImageConversion, t as toStyleString, N as NoImageMetadata, F as FailedToFetchRemoteImageDimensions, b as ExpectedImageOptions, c as ExpectedNotESMImage, d as InvalidImageService, e as createComponent, f as createAstro, g as ImageMissingAlt, m as maybeRenderHead, h as addAttribute, s as spreadAttributes, r as renderTemplate, i as ExperimentalFontsNotEnabled, j as FontFamilyNotFound, u as unescapeHTML } from './astro/server_WO3f6Mge.mjs';
+import { i as isRemoteAllowed } from './remote_BC1y8RCW.mjs';
+import './_@astro-renderers_DpxbEuk7.mjs';
+
+const VALID_SUPPORTED_FORMATS = [
+  "jpeg",
+  "jpg",
+  "png",
+  "tiff",
+  "webp",
+  "gif",
+  "svg",
+  "avif"
+];
+const DEFAULT_OUTPUT_FORMAT = "webp";
+const DEFAULT_HASH_PROPS = [
+  "src",
+  "width",
+  "height",
+  "format",
+  "quality",
+  "fit",
+  "position"
+];
+
+const DEFAULT_RESOLUTIONS = [
+  640,
+  // older and lower-end phones
+  750,
+  // iPhone 6-8
+  828,
+  // iPhone XR/11
+  960,
+  // older horizontal phones
+  1080,
+  // iPhone 6-8 Plus
+  1280,
+  // 720p
+  1668,
+  // Various iPads
+  1920,
+  // 1080p
+  2048,
+  // QXGA
+  2560,
+  // WQXGA
+  3200,
+  // QHD+
+  3840,
+  // 4K
+  4480,
+  // 4.5K
+  5120,
+  // 5K
+  6016
+  // 6K
+];
+const LIMITED_RESOLUTIONS = [
+  640,
+  // older and lower-end phones
+  750,
+  // iPhone 6-8
+  828,
+  // iPhone XR/11
+  1080,
+  // iPhone 6-8 Plus
+  1280,
+  // 720p
+  1668,
+  // Various iPads
+  2048,
+  // QXGA
+  2560
+  // WQXGA
+];
+const getWidths = ({
+  width,
+  layout,
+  breakpoints = DEFAULT_RESOLUTIONS,
+  originalWidth
+}) => {
+  const smallerThanOriginal = (w) => !originalWidth || w <= originalWidth;
+  if (layout === "full-width") {
+    return breakpoints.filter(smallerThanOriginal);
+  }
+  if (!width) {
+    return [];
+  }
+  const doubleWidth = width * 2;
+  const maxSize = originalWidth ? Math.min(doubleWidth, originalWidth) : doubleWidth;
+  if (layout === "fixed") {
+    return originalWidth && width > originalWidth ? [originalWidth] : [width, maxSize];
+  }
+  if (layout === "constrained") {
+    return [
+      // Always include the image at 1x and 2x the specified width
+      width,
+      doubleWidth,
+      ...breakpoints
+    ].filter((w) => w <= maxSize).sort((a, b) => a - b);
+  }
+  return [];
+};
+const getSizesAttribute = ({
+  width,
+  layout
+}) => {
+  if (!width || !layout) {
+    return void 0;
+  }
+  switch (layout) {
+    // If screen is wider than the max size then image width is the max size,
+    // otherwise it's the width of the screen
+    case "constrained":
+      return `(min-width: ${width}px) ${width}px, 100vw`;
+    // Image is always the same width, whatever the size of the screen
+    case "fixed":
+      return `${width}px`;
+    // Image is always the width of the screen
+    case "full-width":
+      return `100vw`;
+    case "none":
+    default:
+      return void 0;
+  }
+};
+
+function isESMImportedImage(src) {
+  return typeof src === "object" || typeof src === "function" && "src" in src;
+}
+function isRemoteImage(src) {
+  return typeof src === "string";
+}
+async function resolveSrc(src) {
+  if (typeof src === "object" && "then" in src) {
+    const resource = await src;
+    return resource.default ?? resource;
+  }
+  return src;
+}
+
+function isLocalService(service) {
+  if (!service) {
+    return false;
+  }
+  return "transform" in service;
+}
+function parseQuality(quality) {
+  let result = parseInt(quality);
+  if (Number.isNaN(result)) {
+    return quality;
+  }
+  return result;
+}
+const sortNumeric = (a, b) => a - b;
+const baseService = {
+  validateOptions(options) {
+    if (!options.src || !isRemoteImage(options.src) && !isESMImportedImage(options.src)) {
+      throw new AstroError({
+        ...ExpectedImage,
+        message: ExpectedImage.message(
+          JSON.stringify(options.src),
+          typeof options.src,
+          JSON.stringify(options, (_, v) => v === void 0 ? null : v)
+        )
+      });
+    }
+    if (!isESMImportedImage(options.src)) {
+      if (options.src.startsWith("/@fs/") || !isRemotePath(options.src) && !options.src.startsWith("/")) {
+        throw new AstroError({
+          ...LocalImageUsedWrongly,
+          message: LocalImageUsedWrongly.message(options.src)
+        });
+      }
+      let missingDimension;
+      if (!options.width && !options.height) {
+        missingDimension = "both";
+      } else if (!options.width && options.height) {
+        missingDimension = "width";
+      } else if (options.width && !options.height) {
+        missingDimension = "height";
+      }
+      if (missingDimension) {
+        throw new AstroError({
+          ...MissingImageDimension,
+          message: MissingImageDimension.message(missingDimension, options.src)
+        });
+      }
+    } else {
+      if (!VALID_SUPPORTED_FORMATS.includes(options.src.format)) {
+        throw new AstroError({
+          ...UnsupportedImageFormat,
+          message: UnsupportedImageFormat.message(
+            options.src.format,
+            options.src.src,
+            VALID_SUPPORTED_FORMATS
+          )
+        });
+      }
+      if (options.widths && options.densities) {
+        throw new AstroError(IncompatibleDescriptorOptions);
+      }
+      if (options.src.format === "svg") {
+        options.format = "svg";
+      }
+      if (options.src.format === "svg" && options.format !== "svg" || options.src.format !== "svg" && options.format === "svg") {
+        throw new AstroError(UnsupportedImageConversion);
+      }
+    }
+    if (!options.format) {
+      options.format = DEFAULT_OUTPUT_FORMAT;
+    }
+    if (options.width) options.width = Math.round(options.width);
+    if (options.height) options.height = Math.round(options.height);
+    if (options.layout && options.width && options.height) {
+      options.fit ??= "cover";
+      delete options.layout;
+    }
+    if (options.fit === "none") {
+      delete options.fit;
+    }
+    return options;
+  },
+  getHTMLAttributes(options) {
+    const { targetWidth, targetHeight } = getTargetDimensions(options);
+    const {
+      src,
+      width,
+      height,
+      format,
+      quality,
+      densities,
+      widths,
+      formats,
+      layout,
+      priority,
+      fit,
+      position,
+      ...attributes
+    } = options;
+    return {
+      ...attributes,
+      width: targetWidth,
+      height: targetHeight,
+      loading: attributes.loading ?? "lazy",
+      decoding: attributes.decoding ?? "async"
+    };
+  },
+  getSrcSet(options) {
+    const { targetWidth, targetHeight } = getTargetDimensions(options);
+    const aspectRatio = targetWidth / targetHeight;
+    const { widths, densities } = options;
+    const targetFormat = options.format ?? DEFAULT_OUTPUT_FORMAT;
+    let transformedWidths = (widths ?? []).sort(sortNumeric);
+    let imageWidth = options.width;
+    let maxWidth = Infinity;
+    if (isESMImportedImage(options.src)) {
+      imageWidth = options.src.width;
+      maxWidth = imageWidth;
+      if (transformedWidths.length > 0 && transformedWidths.at(-1) > maxWidth) {
+        transformedWidths = transformedWidths.filter((width) => width <= maxWidth);
+        transformedWidths.push(maxWidth);
+      }
+    }
+    transformedWidths = Array.from(new Set(transformedWidths));
+    const {
+      width: transformWidth,
+      height: transformHeight,
+      ...transformWithoutDimensions
+    } = options;
+    let allWidths = [];
+    if (densities) {
+      const densityValues = densities.map((density) => {
+        if (typeof density === "number") {
+          return density;
+        } else {
+          return parseFloat(density);
+        }
+      });
+      const densityWidths = densityValues.sort(sortNumeric).map((density) => Math.round(targetWidth * density));
+      allWidths = densityWidths.map((width, index) => ({
+        width,
+        descriptor: `${densityValues[index]}x`
+      }));
+    } else if (transformedWidths.length > 0) {
+      allWidths = transformedWidths.map((width) => ({
+        width,
+        descriptor: `${width}w`
+      }));
+    }
+    return allWidths.map(({ width, descriptor }) => {
+      const height = Math.round(width / aspectRatio);
+      const transform = { ...transformWithoutDimensions, width, height };
+      return {
+        transform,
+        descriptor,
+        attributes: {
+          type: `image/${targetFormat}`
+        }
+      };
+    });
+  },
+  getURL(options, imageConfig) {
+    const searchParams = new URLSearchParams();
+    if (isESMImportedImage(options.src)) {
+      searchParams.append("href", options.src.src);
+    } else if (isRemoteAllowed(options.src, imageConfig)) {
+      searchParams.append("href", options.src);
+    } else {
+      return options.src;
+    }
+    const params = {
+      w: "width",
+      h: "height",
+      q: "quality",
+      f: "format",
+      fit: "fit",
+      position: "position"
+    };
+    Object.entries(params).forEach(([param, key]) => {
+      options[key] && searchParams.append(param, options[key].toString());
+    });
+    const imageEndpoint = joinPaths("/", imageConfig.endpoint.route);
+    return `${imageEndpoint}?${searchParams}`;
+  },
+  parseURL(url) {
+    const params = url.searchParams;
+    if (!params.has("href")) {
+      return void 0;
+    }
+    const transform = {
+      src: params.get("href"),
+      width: params.has("w") ? parseInt(params.get("w")) : void 0,
+      height: params.has("h") ? parseInt(params.get("h")) : void 0,
+      format: params.get("f"),
+      quality: params.get("q"),
+      fit: params.get("fit"),
+      position: params.get("position") ?? void 0
+    };
+    return transform;
+  }
+};
+function getTargetDimensions(options) {
+  let targetWidth = options.width;
+  let targetHeight = options.height;
+  if (isESMImportedImage(options.src)) {
+    const aspectRatio = options.src.width / options.src.height;
+    if (targetHeight && !targetWidth) {
+      targetWidth = Math.round(targetHeight * aspectRatio);
+    } else if (targetWidth && !targetHeight) {
+      targetHeight = Math.round(targetWidth / aspectRatio);
+    } else if (!targetWidth && !targetHeight) {
+      targetWidth = options.src.width;
+      targetHeight = options.src.height;
+    }
+  }
+  return {
+    targetWidth,
+    targetHeight
+  };
+}
+
+function isImageMetadata(src) {
+  return src.fsPath && !("fsPath" in src);
+}
+
+const cssFitValues = ["fill", "contain", "cover", "scale-down"];
+function addCSSVarsToStyle(vars, styles) {
+  const cssVars = Object.entries(vars).filter(([_, value]) => value !== void 0 && value !== false).map(([key, value]) => `--${key}: ${value};`).join(" ");
+  if (!styles) {
+    return cssVars;
+  }
+  const style = typeof styles === "string" ? styles : toStyleString(styles);
+  return `${cssVars} ${style}`;
+}
+
+const decoder = new TextDecoder();
+const toUTF8String = (input, start = 0, end = input.length) => decoder.decode(input.slice(start, end));
+const toHexString = (input, start = 0, end = input.length) => input.slice(start, end).reduce((memo, i) => memo + ("0" + i.toString(16)).slice(-2), "");
+const readInt16LE = (input, offset = 0) => {
+  const val = input[offset] + input[offset + 1] * 2 ** 8;
+  return val | (val & 2 ** 15) * 131070;
+};
+const readUInt16BE = (input, offset = 0) => input[offset] * 2 ** 8 + input[offset + 1];
+const readUInt16LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8;
+const readUInt24LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16;
+const readInt32LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16 + (input[offset + 3] << 24);
+const readUInt32BE = (input, offset = 0) => input[offset] * 2 ** 24 + input[offset + 1] * 2 ** 16 + input[offset + 2] * 2 ** 8 + input[offset + 3];
+const readUInt32LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16 + input[offset + 3] * 2 ** 24;
+const methods = {
+  readUInt16BE,
+  readUInt16LE,
+  readUInt32BE,
+  readUInt32LE
+};
+function readUInt(input, bits, offset, isBigEndian) {
+  offset = offset || 0;
+  const endian = isBigEndian ? "BE" : "LE";
+  const methodName = "readUInt" + bits + endian;
+  return methods[methodName](input, offset);
+}
+function readBox(buffer, offset) {
+  if (buffer.length - offset < 4) return;
+  const boxSize = readUInt32BE(buffer, offset);
+  if (buffer.length - offset < boxSize) return;
+  return {
+    name: toUTF8String(buffer, 4 + offset, 8 + offset),
+    offset,
+    size: boxSize
+  };
+}
+function findBox(buffer, boxName, offset) {
+  while (offset < buffer.length) {
+    const box = readBox(buffer, offset);
+    if (!box) break;
+    if (box.name === boxName) return box;
+    offset += box.size;
+  }
+}
+
+const BMP = {
+  validate: (input) => toUTF8String(input, 0, 2) === "BM",
+  calculate: (input) => ({
+    height: Math.abs(readInt32LE(input, 22)),
+    width: readUInt32LE(input, 18)
+  })
+};
+
+const TYPE_ICON = 1;
+const SIZE_HEADER$1 = 2 + 2 + 2;
+const SIZE_IMAGE_ENTRY = 1 + 1 + 1 + 1 + 2 + 2 + 4 + 4;
+function getSizeFromOffset(input, offset) {
+  const value = input[offset];
+  return value === 0 ? 256 : value;
+}
+function getImageSize$1(input, imageIndex) {
+  const offset = SIZE_HEADER$1 + imageIndex * SIZE_IMAGE_ENTRY;
+  return {
+    height: getSizeFromOffset(input, offset + 1),
+    width: getSizeFromOffset(input, offset)
+  };
+}
+const ICO = {
+  validate(input) {
+    const reserved = readUInt16LE(input, 0);
+    const imageCount = readUInt16LE(input, 4);
+    if (reserved !== 0 || imageCount === 0) return false;
+    const imageType = readUInt16LE(input, 2);
+    return imageType === TYPE_ICON;
+  },
+  calculate(input) {
+    const nbImages = readUInt16LE(input, 4);
+    const imageSize = getImageSize$1(input, 0);
+    if (nbImages === 1) return imageSize;
+    const imgs = [imageSize];
+    for (let imageIndex = 1; imageIndex < nbImages; imageIndex += 1) {
+      imgs.push(getImageSize$1(input, imageIndex));
+    }
+    return {
+      height: imageSize.height,
+      images: imgs,
+      width: imageSize.width
+    };
+  }
+};
+
+const TYPE_CURSOR = 2;
+const CUR = {
+  validate(input) {
+    const reserved = readUInt16LE(input, 0);
+    const imageCount = readUInt16LE(input, 4);
+    if (reserved !== 0 || imageCount === 0) return false;
+    const imageType = readUInt16LE(input, 2);
+    return imageType === TYPE_CURSOR;
+  },
+  calculate: (input) => ICO.calculate(input)
+};
+
+const DDS = {
+  validate: (input) => readUInt32LE(input, 0) === 542327876,
+  calculate: (input) => ({
+    height: readUInt32LE(input, 12),
+    width: readUInt32LE(input, 16)
+  })
+};
+
+const gifRegexp = /^GIF8[79]a/;
+const GIF = {
+  validate: (input) => gifRegexp.test(toUTF8String(input, 0, 6)),
+  calculate: (input) => ({
+    height: readUInt16LE(input, 8),
+    width: readUInt16LE(input, 6)
+  })
+};
+
+const brandMap = {
+  avif: "avif",
+  avis: "avif",
+  // avif-sequence
+  mif1: "heif",
+  msf1: "heif",
+  // heif-sequence
+  heic: "heic",
+  heix: "heic",
+  hevc: "heic",
+  // heic-sequence
+  hevx: "heic"
+  // heic-sequence
+};
+function detectBrands(buffer, start, end) {
+  let brandsDetected = {};
+  for (let i = start; i <= end; i += 4) {
+    const brand = toUTF8String(buffer, i, i + 4);
+    if (brand in brandMap) {
+      brandsDetected[brand] = 1;
+    }
+  }
+  if ("avif" in brandsDetected || "avis" in brandsDetected) {
+    return "avif";
+  } else if ("heic" in brandsDetected || "heix" in brandsDetected || "hevc" in brandsDetected || "hevx" in brandsDetected) {
+    return "heic";
+  } else if ("mif1" in brandsDetected || "msf1" in brandsDetected) {
+    return "heif";
+  }
+}
+const HEIF = {
+  validate(buffer) {
+    const ftype = toUTF8String(buffer, 4, 8);
+    const brand = toUTF8String(buffer, 8, 12);
+    return "ftyp" === ftype && brand in brandMap;
+  },
+  calculate(buffer) {
+    const metaBox = findBox(buffer, "meta", 0);
+    const iprpBox = metaBox && findBox(buffer, "iprp", metaBox.offset + 12);
+    const ipcoBox = iprpBox && findBox(buffer, "ipco", iprpBox.offset + 8);
+    const ispeBox = ipcoBox && findBox(buffer, "ispe", ipcoBox.offset + 8);
+    if (ispeBox) {
+      return {
+        height: readUInt32BE(buffer, ispeBox.offset + 16),
+        width: readUInt32BE(buffer, ispeBox.offset + 12),
+        type: detectBrands(buffer, 8, metaBox.offset)
+      };
+    }
+    throw new TypeError("Invalid HEIF, no size found");
+  }
+};
+
+const SIZE_HEADER = 4 + 4;
+const FILE_LENGTH_OFFSET = 4;
+const ENTRY_LENGTH_OFFSET = 4;
+const ICON_TYPE_SIZE = {
+  ICON: 32,
+  "ICN#": 32,
+  // m => 16 x 16
+  "icm#": 16,
+  icm4: 16,
+  icm8: 16,
+  // s => 16 x 16
+  "ics#": 16,
+  ics4: 16,
+  ics8: 16,
+  is32: 16,
+  s8mk: 16,
+  icp4: 16,
+  // l => 32 x 32
+  icl4: 32,
+  icl8: 32,
+  il32: 32,
+  l8mk: 32,
+  icp5: 32,
+  ic11: 32,
+  // h => 48 x 48
+  ich4: 48,
+  ich8: 48,
+  ih32: 48,
+  h8mk: 48,
+  // . => 64 x 64
+  icp6: 64,
+  ic12: 32,
+  // t => 128 x 128
+  it32: 128,
+  t8mk: 128,
+  ic07: 128,
+  // . => 256 x 256
+  ic08: 256,
+  ic13: 256,
+  // . => 512 x 512
+  ic09: 512,
+  ic14: 512,
+  // . => 1024 x 1024
+  ic10: 1024
+};
+function readImageHeader(input, imageOffset) {
+  const imageLengthOffset = imageOffset + ENTRY_LENGTH_OFFSET;
+  return [
+    toUTF8String(input, imageOffset, imageLengthOffset),
+    readUInt32BE(input, imageLengthOffset)
+  ];
+}
+function getImageSize(type) {
+  const size = ICON_TYPE_SIZE[type];
+  return { width: size, height: size, type };
+}
+const ICNS = {
+  validate: (input) => toUTF8String(input, 0, 4) === "icns",
+  calculate(input) {
+    const inputLength = input.length;
+    const fileLength = readUInt32BE(input, FILE_LENGTH_OFFSET);
+    let imageOffset = SIZE_HEADER;
+    let imageHeader = readImageHeader(input, imageOffset);
+    let imageSize = getImageSize(imageHeader[0]);
+    imageOffset += imageHeader[1];
+    if (imageOffset === fileLength) return imageSize;
+    const result = {
+      height: imageSize.height,
+      images: [imageSize],
+      width: imageSize.width
+    };
+    while (imageOffset < fileLength && imageOffset < inputLength) {
+      imageHeader = readImageHeader(input, imageOffset);
+      imageSize = getImageSize(imageHeader[0]);
+      imageOffset += imageHeader[1];
+      result.images.push(imageSize);
+    }
+    return result;
+  }
+};
+
+const J2C = {
+  // TODO: this doesn't seem right. SIZ marker doesn't have to be right after the SOC
+  validate: (input) => toHexString(input, 0, 4) === "ff4fff51",
+  calculate: (input) => ({
+    height: readUInt32BE(input, 12),
+    width: readUInt32BE(input, 8)
+  })
+};
+
+const JP2 = {
+  validate(input) {
+    if (readUInt32BE(input, 4) !== 1783636e3 || readUInt32BE(input, 0) < 1) return false;
+    const ftypBox = findBox(input, "ftyp", 0);
+    if (!ftypBox) return false;
+    return readUInt32BE(input, ftypBox.offset + 4) === 1718909296;
+  },
+  calculate(input) {
+    const jp2hBox = findBox(input, "jp2h", 0);
+    const ihdrBox = jp2hBox && findBox(input, "ihdr", jp2hBox.offset + 8);
+    if (ihdrBox) {
+      return {
+        height: readUInt32BE(input, ihdrBox.offset + 8),
+        width: readUInt32BE(input, ihdrBox.offset + 12)
+      };
+    }
+    throw new TypeError("Unsupported JPEG 2000 format");
+  }
+};
+
+const EXIF_MARKER = "45786966";
+const APP1_DATA_SIZE_BYTES = 2;
+const EXIF_HEADER_BYTES = 6;
+const TIFF_BYTE_ALIGN_BYTES = 2;
+const BIG_ENDIAN_BYTE_ALIGN = "4d4d";
+const LITTLE_ENDIAN_BYTE_ALIGN = "4949";
+const IDF_ENTRY_BYTES = 12;
+const NUM_DIRECTORY_ENTRIES_BYTES = 2;
+function isEXIF(input) {
+  return toHexString(input, 2, 6) === EXIF_MARKER;
+}
+function extractSize(input, index) {
+  return {
+    height: readUInt16BE(input, index),
+    width: readUInt16BE(input, index + 2)
+  };
+}
+function extractOrientation(exifBlock, isBigEndian) {
+  const idfOffset = 8;
+  const offset = EXIF_HEADER_BYTES + idfOffset;
+  const idfDirectoryEntries = readUInt(exifBlock, 16, offset, isBigEndian);
+  for (let directoryEntryNumber = 0; directoryEntryNumber < idfDirectoryEntries; directoryEntryNumber++) {
+    const start = offset + NUM_DIRECTORY_ENTRIES_BYTES + directoryEntryNumber * IDF_ENTRY_BYTES;
+    const end = start + IDF_ENTRY_BYTES;
+    if (start > exifBlock.length) {
+      return;
+    }
+    const block = exifBlock.slice(start, end);
+    const tagNumber = readUInt(block, 16, 0, isBigEndian);
+    if (tagNumber === 274) {
+      const dataFormat = readUInt(block, 16, 2, isBigEndian);
+      if (dataFormat !== 3) {
+        return;
+      }
+      const numberOfComponents = readUInt(block, 32, 4, isBigEndian);
+      if (numberOfComponents !== 1) {
+        return;
+      }
+      return readUInt(block, 16, 8, isBigEndian);
+    }
+  }
+}
+function validateExifBlock(input, index) {
+  const exifBlock = input.slice(APP1_DATA_SIZE_BYTES, index);
+  const byteAlign = toHexString(
+    exifBlock,
+    EXIF_HEADER_BYTES,
+    EXIF_HEADER_BYTES + TIFF_BYTE_ALIGN_BYTES
+  );
+  const isBigEndian = byteAlign === BIG_ENDIAN_BYTE_ALIGN;
+  const isLittleEndian = byteAlign === LITTLE_ENDIAN_BYTE_ALIGN;
+  if (isBigEndian || isLittleEndian) {
+    return extractOrientation(exifBlock, isBigEndian);
+  }
+}
+function validateInput(input, index) {
+  if (index > input.length) {
+    throw new TypeError("Corrupt JPG, exceeded buffer limits");
+  }
+}
+const JPG = {
+  validate: (input) => toHexString(input, 0, 2) === "ffd8",
+  calculate(input) {
+    input = input.slice(4);
+    let orientation;
+    let next;
+    while (input.length) {
+      const i = readUInt16BE(input, 0);
+      if (input[i] !== 255) {
+        input = input.slice(i);
+        continue;
+      }
+      if (isEXIF(input)) {
+        orientation = validateExifBlock(input, i);
+      }
+      validateInput(input, i);
+      next = input[i + 1];
+      if (next === 192 || next === 193 || next === 194) {
+        const size = extractSize(input, i + 5);
+        if (!orientation) {
+          return size;
+        }
+        return {
+          height: size.height,
+          orientation,
+          width: size.width
+        };
+      }
+      input = input.slice(i + 2);
+    }
+    throw new TypeError("Invalid JPG, no size found");
+  }
+};
+
+const KTX = {
+  validate: (input) => {
+    const signature = toUTF8String(input, 1, 7);
+    return ["KTX 11", "KTX 20"].includes(signature);
+  },
+  calculate: (input) => {
+    const type = input[5] === 49 ? "ktx" : "ktx2";
+    const offset = type === "ktx" ? 36 : 20;
+    return {
+      height: readUInt32LE(input, offset + 4),
+      width: readUInt32LE(input, offset),
+      type
+    };
+  }
+};
+
+const pngSignature = "PNG\r\n\n";
+const pngImageHeaderChunkName = "IHDR";
+const pngFriedChunkName = "CgBI";
+const PNG = {
+  validate(input) {
+    if (pngSignature === toUTF8String(input, 1, 8)) {
+      let chunkName = toUTF8String(input, 12, 16);
+      if (chunkName === pngFriedChunkName) {
+        chunkName = toUTF8String(input, 28, 32);
+      }
+      if (chunkName !== pngImageHeaderChunkName) {
+        throw new TypeError("Invalid PNG");
+      }
+      return true;
+    }
+    return false;
+  },
+  calculate(input) {
+    if (toUTF8String(input, 12, 16) === pngFriedChunkName) {
+      return {
+        height: readUInt32BE(input, 36),
+        width: readUInt32BE(input, 32)
+      };
+    }
+    return {
+      height: readUInt32BE(input, 20),
+      width: readUInt32BE(input, 16)
+    };
+  }
+};
+
+const PNMTypes = {
+  P1: "pbm/ascii",
+  P2: "pgm/ascii",
+  P3: "ppm/ascii",
+  P4: "pbm",
+  P5: "pgm",
+  P6: "ppm",
+  P7: "pam",
+  PF: "pfm"
+};
+const handlers = {
+  default: (lines) => {
+    let dimensions = [];
+    while (lines.length > 0) {
+      const line = lines.shift();
+      if (line[0] === "#") {
+        continue;
+      }
+      dimensions = line.split(" ");
+      break;
+    }
+    if (dimensions.length === 2) {
+      return {
+        height: parseInt(dimensions[1], 10),
+        width: parseInt(dimensions[0], 10)
+      };
+    } else {
+      throw new TypeError("Invalid PNM");
+    }
+  },
+  pam: (lines) => {
+    const size = {};
+    while (lines.length > 0) {
+      const line = lines.shift();
+      if (line.length > 16 || line.charCodeAt(0) > 128) {
+        continue;
+      }
+      const [key, value] = line.split(" ");
+      if (key && value) {
+        size[key.toLowerCase()] = parseInt(value, 10);
+      }
+      if (size.height && size.width) {
+        break;
+      }
+    }
+    if (size.height && size.width) {
+      return {
+        height: size.height,
+        width: size.width
+      };
+    } else {
+      throw new TypeError("Invalid PAM");
+    }
+  }
+};
+const PNM = {
+  validate: (input) => toUTF8String(input, 0, 2) in PNMTypes,
+  calculate(input) {
+    const signature = toUTF8String(input, 0, 2);
+    const type = PNMTypes[signature];
+    const lines = toUTF8String(input, 3).split(/[\r\n]+/);
+    const handler = handlers[type] || handlers.default;
+    return handler(lines);
+  }
+};
+
+const PSD = {
+  validate: (input) => toUTF8String(input, 0, 4) === "8BPS",
+  calculate: (input) => ({
+    height: readUInt32BE(input, 14),
+    width: readUInt32BE(input, 18)
+  })
+};
+
+const svgReg = /<svg\s([^>"']|"[^"]*"|'[^']*')*>/;
+const extractorRegExps = {
+  height: /\sheight=(['"])([^%]+?)\1/,
+  root: svgReg,
+  viewbox: /\sviewBox=(['"])(.+?)\1/i,
+  width: /\swidth=(['"])([^%]+?)\1/
+};
+const INCH_CM = 2.54;
+const units = {
+  in: 96,
+  cm: 96 / INCH_CM,
+  em: 16,
+  ex: 8,
+  m: 96 / INCH_CM * 100,
+  mm: 96 / INCH_CM / 10,
+  pc: 96 / 72 / 12,
+  pt: 96 / 72,
+  px: 1
+};
+const unitsReg = new RegExp(
+  `^([0-9.]+(?:e\\d+)?)(${Object.keys(units).join("|")})?$`
+);
+function parseLength(len) {
+  const m = unitsReg.exec(len);
+  if (!m) {
+    return void 0;
+  }
+  return Math.round(Number(m[1]) * (units[m[2]] || 1));
+}
+function parseViewbox(viewbox) {
+  const bounds = viewbox.split(" ");
+  return {
+    height: parseLength(bounds[3]),
+    width: parseLength(bounds[2])
+  };
+}
+function parseAttributes(root) {
+  const width = extractorRegExps.width.exec(root);
+  const height = extractorRegExps.height.exec(root);
+  const viewbox = extractorRegExps.viewbox.exec(root);
+  return {
+    height: height && parseLength(height[2]),
+    viewbox: viewbox && parseViewbox(viewbox[2]),
+    width: width && parseLength(width[2])
+  };
+}
+function calculateByDimensions(attrs) {
+  return {
+    height: attrs.height,
+    width: attrs.width
+  };
+}
+function calculateByViewbox(attrs, viewbox) {
+  const ratio = viewbox.width / viewbox.height;
+  if (attrs.width) {
+    return {
+      height: Math.floor(attrs.width / ratio),
+      width: attrs.width
+    };
+  }
+  if (attrs.height) {
+    return {
+      height: attrs.height,
+      width: Math.floor(attrs.height * ratio)
+    };
+  }
+  return {
+    height: viewbox.height,
+    width: viewbox.width
+  };
+}
+const SVG = {
+  // Scan only the first kilo-byte to speed up the check on larger files
+  validate: (input) => svgReg.test(toUTF8String(input, 0, 1e3)),
+  calculate(input) {
+    const root = extractorRegExps.root.exec(toUTF8String(input));
+    if (root) {
+      const attrs = parseAttributes(root[0]);
+      if (attrs.width && attrs.height) {
+        return calculateByDimensions(attrs);
+      }
+      if (attrs.viewbox) {
+        return calculateByViewbox(attrs, attrs.viewbox);
+      }
+    }
+    throw new TypeError("Invalid SVG");
+  }
+};
+
+const TGA = {
+  validate(input) {
+    return readUInt16LE(input, 0) === 0 && readUInt16LE(input, 4) === 0;
+  },
+  calculate(input) {
+    return {
+      height: readUInt16LE(input, 14),
+      width: readUInt16LE(input, 12)
+    };
+  }
+};
+
+function readIFD(input, isBigEndian) {
+  const ifdOffset = readUInt(input, 32, 4, isBigEndian);
+  return input.slice(ifdOffset + 2);
+}
+function readValue(input, isBigEndian) {
+  const low = readUInt(input, 16, 8, isBigEndian);
+  const high = readUInt(input, 16, 10, isBigEndian);
+  return (high << 16) + low;
+}
+function nextTag(input) {
+  if (input.length > 24) {
+    return input.slice(12);
+  }
+}
+function extractTags(input, isBigEndian) {
+  const tags = {};
+  let temp = input;
+  while (temp && temp.length) {
+    const code = readUInt(temp, 16, 0, isBigEndian);
+    const type = readUInt(temp, 16, 2, isBigEndian);
+    const length = readUInt(temp, 32, 4, isBigEndian);
+    if (code === 0) {
+      break;
+    } else {
+      if (length === 1 && (type === 3 || type === 4)) {
+        tags[code] = readValue(temp, isBigEndian);
+      }
+      temp = nextTag(temp);
+    }
+  }
+  return tags;
+}
+function determineEndianness(input) {
+  const signature = toUTF8String(input, 0, 2);
+  if ("II" === signature) {
+    return "LE";
+  } else if ("MM" === signature) {
+    return "BE";
+  }
+}
+const signatures = [
+  // '492049', // currently not supported
+  "49492a00",
+  // Little endian
+  "4d4d002a"
+  // Big Endian
+  // '4d4d002a', // BigTIFF > 4GB. currently not supported
+];
+const TIFF = {
+  validate: (input) => signatures.includes(toHexString(input, 0, 4)),
+  calculate(input) {
+    const isBigEndian = determineEndianness(input) === "BE";
+    const ifdBuffer = readIFD(input, isBigEndian);
+    const tags = extractTags(ifdBuffer, isBigEndian);
+    const width = tags[256];
+    const height = tags[257];
+    if (!width || !height) {
+      throw new TypeError("Invalid Tiff. Missing tags");
+    }
+    return { height, width };
+  }
+};
+
+function calculateExtended(input) {
+  return {
+    height: 1 + readUInt24LE(input, 7),
+    width: 1 + readUInt24LE(input, 4)
+  };
+}
+function calculateLossless(input) {
+  return {
+    height: 1 + ((input[4] & 15) << 10 | input[3] << 2 | (input[2] & 192) >> 6),
+    width: 1 + ((input[2] & 63) << 8 | input[1])
+  };
+}
+function calculateLossy(input) {
+  return {
+    height: readInt16LE(input, 8) & 16383,
+    width: readInt16LE(input, 6) & 16383
+  };
+}
+const WEBP = {
+  validate(input) {
+    const riffHeader = "RIFF" === toUTF8String(input, 0, 4);
+    const webpHeader = "WEBP" === toUTF8String(input, 8, 12);
+    const vp8Header = "VP8" === toUTF8String(input, 12, 15);
+    return riffHeader && webpHeader && vp8Header;
+  },
+  calculate(input) {
+    const chunkHeader = toUTF8String(input, 12, 16);
+    input = input.slice(20, 30);
+    if (chunkHeader === "VP8X") {
+      const extendedHeader = input[0];
+      const validStart = (extendedHeader & 192) === 0;
+      const validEnd = (extendedHeader & 1) === 0;
+      if (validStart && validEnd) {
+        return calculateExtended(input);
+      } else {
+        throw new TypeError("Invalid WebP");
+      }
+    }
+    if (chunkHeader === "VP8 " && input[0] !== 47) {
+      return calculateLossy(input);
+    }
+    const signature = toHexString(input, 3, 6);
+    if (chunkHeader === "VP8L" && signature !== "9d012a") {
+      return calculateLossless(input);
+    }
+    throw new TypeError("Invalid WebP");
+  }
+};
+
+const typeHandlers = /* @__PURE__ */ new Map([
+  ["bmp", BMP],
+  ["cur", CUR],
+  ["dds", DDS],
+  ["gif", GIF],
+  ["heif", HEIF],
+  ["icns", ICNS],
+  ["ico", ICO],
+  ["j2c", J2C],
+  ["jp2", JP2],
+  ["jpg", JPG],
+  ["ktx", KTX],
+  ["png", PNG],
+  ["pnm", PNM],
+  ["psd", PSD],
+  ["svg", SVG],
+  ["tga", TGA],
+  ["tiff", TIFF],
+  ["webp", WEBP]
+]);
+const types = Array.from(typeHandlers.keys());
+
+const firstBytes = /* @__PURE__ */ new Map([
+  [56, "psd"],
+  [66, "bmp"],
+  [68, "dds"],
+  [71, "gif"],
+  [73, "tiff"],
+  [77, "tiff"],
+  [82, "webp"],
+  [105, "icns"],
+  [137, "png"],
+  [255, "jpg"]
+]);
+function detector(input) {
+  const byte = input[0];
+  const type = firstBytes.get(byte);
+  if (type && typeHandlers.get(type).validate(input)) {
+    return type;
+  }
+  return types.find((fileType) => typeHandlers.get(fileType).validate(input));
+}
+
+function lookup$1(input) {
+  const type = detector(input);
+  if (typeof type !== "undefined") {
+    const size = typeHandlers.get(type).calculate(input);
+    if (size !== void 0) {
+      size.type = size.type ?? type;
+      return size;
+    }
+  }
+  throw new TypeError("unsupported file type: " + type);
+}
+
+async function imageMetadata(data, src) {
+  let result;
+  try {
+    result = lookup$1(data);
+  } catch {
+    throw new AstroError({
+      ...NoImageMetadata,
+      message: NoImageMetadata.message(src)
+    });
+  }
+  if (!result.height || !result.width || !result.type) {
+    throw new AstroError({
+      ...NoImageMetadata,
+      message: NoImageMetadata.message(src)
+    });
+  }
+  const { width, height, type, orientation } = result;
+  const isPortrait = (orientation || 0) >= 5;
+  return {
+    width: isPortrait ? height : width,
+    height: isPortrait ? width : height,
+    format: type,
+    orientation
+  };
+}
+
+async function inferRemoteSize(url) {
+  const response = await fetch(url);
+  if (!response.body || !response.ok) {
+    throw new AstroError({
+      ...FailedToFetchRemoteImageDimensions,
+      message: FailedToFetchRemoteImageDimensions.message(url)
+    });
+  }
+  const reader = response.body.getReader();
+  let done, value;
+  let accumulatedChunks = new Uint8Array();
+  while (!done) {
+    const readResult = await reader.read();
+    done = readResult.done;
+    if (done) break;
+    if (readResult.value) {
+      value = readResult.value;
+      let tmp = new Uint8Array(accumulatedChunks.length + value.length);
+      tmp.set(accumulatedChunks, 0);
+      tmp.set(value, accumulatedChunks.length);
+      accumulatedChunks = tmp;
+      try {
+        const dimensions = await imageMetadata(accumulatedChunks, url);
+        if (dimensions) {
+          await reader.cancel();
+          return dimensions;
+        }
+      } catch {
+      }
+    }
+  }
+  throw new AstroError({
+    ...NoImageMetadata,
+    message: NoImageMetadata.message(url)
+  });
+}
+
+async function getConfiguredImageService() {
+  if (!globalThis?.astroAsset?.imageService) {
+    const { default: service } = await import(
+      // @ts-expect-error
+      './sharp_CG4EIo5e.mjs'
+    ).catch((e) => {
+      const error = new AstroError(InvalidImageService);
+      error.cause = e;
+      throw error;
+    });
+    if (!globalThis.astroAsset) globalThis.astroAsset = {};
+    globalThis.astroAsset.imageService = service;
+    return service;
+  }
+  return globalThis.astroAsset.imageService;
+}
+async function getImage$1(options, imageConfig) {
+  if (!options || typeof options !== "object") {
+    throw new AstroError({
+      ...ExpectedImageOptions,
+      message: ExpectedImageOptions.message(JSON.stringify(options))
+    });
+  }
+  if (typeof options.src === "undefined") {
+    throw new AstroError({
+      ...ExpectedImage,
+      message: ExpectedImage.message(
+        options.src,
+        "undefined",
+        JSON.stringify(options)
+      )
+    });
+  }
+  if (isImageMetadata(options)) {
+    throw new AstroError(ExpectedNotESMImage);
+  }
+  const service = await getConfiguredImageService();
+  const resolvedOptions = {
+    ...options,
+    src: await resolveSrc(options.src)
+  };
+  let originalWidth;
+  let originalHeight;
+  if (options.inferSize && isRemoteImage(resolvedOptions.src) && isRemotePath(resolvedOptions.src)) {
+    const result = await inferRemoteSize(resolvedOptions.src);
+    resolvedOptions.width ??= result.width;
+    resolvedOptions.height ??= result.height;
+    originalWidth = result.width;
+    originalHeight = result.height;
+    delete resolvedOptions.inferSize;
+  }
+  const originalFilePath = isESMImportedImage(resolvedOptions.src) ? resolvedOptions.src.fsPath : void 0;
+  const clonedSrc = isESMImportedImage(resolvedOptions.src) ? (
+    // @ts-expect-error - clone is a private, hidden prop
+    resolvedOptions.src.clone ?? resolvedOptions.src
+  ) : resolvedOptions.src;
+  if (isESMImportedImage(clonedSrc)) {
+    originalWidth = clonedSrc.width;
+    originalHeight = clonedSrc.height;
+  }
+  if (originalWidth && originalHeight) {
+    const aspectRatio = originalWidth / originalHeight;
+    if (resolvedOptions.height && !resolvedOptions.width) {
+      resolvedOptions.width = Math.round(resolvedOptions.height * aspectRatio);
+    } else if (resolvedOptions.width && !resolvedOptions.height) {
+      resolvedOptions.height = Math.round(resolvedOptions.width / aspectRatio);
+    } else if (!resolvedOptions.width && !resolvedOptions.height) {
+      resolvedOptions.width = originalWidth;
+      resolvedOptions.height = originalHeight;
+    }
+  }
+  resolvedOptions.src = clonedSrc;
+  const layout = options.layout ?? imageConfig.layout ?? "none";
+  if (resolvedOptions.priority) {
+    resolvedOptions.loading ??= "eager";
+    resolvedOptions.decoding ??= "sync";
+    resolvedOptions.fetchpriority ??= "high";
+    delete resolvedOptions.priority;
+  } else {
+    resolvedOptions.loading ??= "lazy";
+    resolvedOptions.decoding ??= "async";
+    resolvedOptions.fetchpriority ??= "auto";
+  }
+  if (layout !== "none") {
+    resolvedOptions.widths ||= getWidths({
+      width: resolvedOptions.width,
+      layout,
+      originalWidth,
+      breakpoints: imageConfig.breakpoints?.length ? imageConfig.breakpoints : isLocalService(service) ? LIMITED_RESOLUTIONS : DEFAULT_RESOLUTIONS
+    });
+    resolvedOptions.sizes ||= getSizesAttribute({ width: resolvedOptions.width, layout });
+    delete resolvedOptions.densities;
+    resolvedOptions.style = addCSSVarsToStyle(
+      {
+        fit: cssFitValues.includes(resolvedOptions.fit ?? "") && resolvedOptions.fit,
+        pos: resolvedOptions.position
+      },
+      resolvedOptions.style
+    );
+    resolvedOptions["data-astro-image"] = layout;
+  }
+  const validatedOptions = service.validateOptions ? await service.validateOptions(resolvedOptions, imageConfig) : resolvedOptions;
+  const srcSetTransforms = service.getSrcSet ? await service.getSrcSet(validatedOptions, imageConfig) : [];
+  let imageURL = await service.getURL(validatedOptions, imageConfig);
+  const matchesValidatedTransform = (transform) => transform.width === validatedOptions.width && transform.height === validatedOptions.height && transform.format === validatedOptions.format;
+  let srcSets = await Promise.all(
+    srcSetTransforms.map(async (srcSet) => {
+      return {
+        transform: srcSet.transform,
+        url: matchesValidatedTransform(srcSet.transform) ? imageURL : await service.getURL(srcSet.transform, imageConfig),
+        descriptor: srcSet.descriptor,
+        attributes: srcSet.attributes
+      };
+    })
+  );
+  if (isLocalService(service) && globalThis.astroAsset.addStaticImage && !(isRemoteImage(validatedOptions.src) && imageURL === validatedOptions.src)) {
+    const propsToHash = service.propertiesToHash ?? DEFAULT_HASH_PROPS;
+    imageURL = globalThis.astroAsset.addStaticImage(
+      validatedOptions,
+      propsToHash,
+      originalFilePath
+    );
+    srcSets = srcSetTransforms.map((srcSet) => {
+      return {
+        transform: srcSet.transform,
+        url: matchesValidatedTransform(srcSet.transform) ? imageURL : globalThis.astroAsset.addStaticImage(srcSet.transform, propsToHash, originalFilePath),
+        descriptor: srcSet.descriptor,
+        attributes: srcSet.attributes
+      };
+    });
+  }
+  return {
+    rawOptions: resolvedOptions,
+    options: validatedOptions,
+    src: imageURL,
+    srcSet: {
+      values: srcSets,
+      attribute: srcSets.map((srcSet) => `${srcSet.url} ${srcSet.descriptor}`).join(", ")
+    },
+    attributes: service.getHTMLAttributes !== void 0 ? await service.getHTMLAttributes(validatedOptions, imageConfig) : {}
+  };
+}
+
+const $$Astro$2 = createAstro();
+const $$Image = createComponent(async ($$result, $$props, $$slots) => {
+  const Astro2 = $$result.createAstro($$Astro$2, $$props, $$slots);
+  Astro2.self = $$Image;
+  const props = Astro2.props;
+  if (props.alt === void 0 || props.alt === null) {
+    throw new AstroError(ImageMissingAlt);
+  }
+  if (typeof props.width === "string") {
+    props.width = parseInt(props.width);
+  }
+  if (typeof props.height === "string") {
+    props.height = parseInt(props.height);
+  }
+  const layout = props.layout ?? imageConfig.layout ?? "none";
+  if (layout !== "none") {
+    props.layout ??= imageConfig.layout;
+    props.fit ??= imageConfig.objectFit ?? "cover";
+    props.position ??= imageConfig.objectPosition ?? "center";
+  }
+  const image = await getImage(props);
+  const additionalAttributes = {};
+  if (image.srcSet.values.length > 0) {
+    additionalAttributes.srcset = image.srcSet.attribute;
+  }
+  const { class: className, ...attributes } = { ...additionalAttributes, ...image.attributes };
+  return renderTemplate`${maybeRenderHead()}<img${addAttribute(image.src, "src")}${spreadAttributes(attributes)}${addAttribute(className, "class")}>`;
+}, "/home/fc/Projects/glance/node_modules/astro/components/Image.astro", void 0);
+
+const mimes = {
+  "3g2": "video/3gpp2",
+  "3gp": "video/3gpp",
+  "3gpp": "video/3gpp",
+  "3mf": "model/3mf",
+  "aac": "audio/aac",
+  "ac": "application/pkix-attr-cert",
+  "adp": "audio/adpcm",
+  "adts": "audio/aac",
+  "ai": "application/postscript",
+  "aml": "application/automationml-aml+xml",
+  "amlx": "application/automationml-amlx+zip",
+  "amr": "audio/amr",
+  "apng": "image/apng",
+  "appcache": "text/cache-manifest",
+  "appinstaller": "application/appinstaller",
+  "appx": "application/appx",
+  "appxbundle": "application/appxbundle",
+  "asc": "application/pgp-keys",
+  "atom": "application/atom+xml",
+  "atomcat": "application/atomcat+xml",
+  "atomdeleted": "application/atomdeleted+xml",
+  "atomsvc": "application/atomsvc+xml",
+  "au": "audio/basic",
+  "avci": "image/avci",
+  "avcs": "image/avcs",
+  "avif": "image/avif",
+  "aw": "application/applixware",
+  "bdoc": "application/bdoc",
+  "bin": "application/octet-stream",
+  "bmp": "image/bmp",
+  "bpk": "application/octet-stream",
+  "btf": "image/prs.btif",
+  "btif": "image/prs.btif",
+  "buffer": "application/octet-stream",
+  "ccxml": "application/ccxml+xml",
+  "cdfx": "application/cdfx+xml",
+  "cdmia": "application/cdmi-capability",
+  "cdmic": "application/cdmi-container",
+  "cdmid": "application/cdmi-domain",
+  "cdmio": "application/cdmi-object",
+  "cdmiq": "application/cdmi-queue",
+  "cer": "application/pkix-cert",
+  "cgm": "image/cgm",
+  "cjs": "application/node",
+  "class": "application/java-vm",
+  "coffee": "text/coffeescript",
+  "conf": "text/plain",
+  "cpl": "application/cpl+xml",
+  "cpt": "application/mac-compactpro",
+  "crl": "application/pkix-crl",
+  "css": "text/css",
+  "csv": "text/csv",
+  "cu": "application/cu-seeme",
+  "cwl": "application/cwl",
+  "cww": "application/prs.cww",
+  "davmount": "application/davmount+xml",
+  "dbk": "application/docbook+xml",
+  "deb": "application/octet-stream",
+  "def": "text/plain",
+  "deploy": "application/octet-stream",
+  "dib": "image/bmp",
+  "disposition-notification": "message/disposition-notification",
+  "dist": "application/octet-stream",
+  "distz": "application/octet-stream",
+  "dll": "application/octet-stream",
+  "dmg": "application/octet-stream",
+  "dms": "application/octet-stream",
+  "doc": "application/msword",
+  "dot": "application/msword",
+  "dpx": "image/dpx",
+  "drle": "image/dicom-rle",
+  "dsc": "text/prs.lines.tag",
+  "dssc": "application/dssc+der",
+  "dtd": "application/xml-dtd",
+  "dump": "application/octet-stream",
+  "dwd": "application/atsc-dwd+xml",
+  "ear": "application/java-archive",
+  "ecma": "application/ecmascript",
+  "elc": "application/octet-stream",
+  "emf": "image/emf",
+  "eml": "message/rfc822",
+  "emma": "application/emma+xml",
+  "emotionml": "application/emotionml+xml",
+  "eps": "application/postscript",
+  "epub": "application/epub+zip",
+  "exe": "application/octet-stream",
+  "exi": "application/exi",
+  "exp": "application/express",
+  "exr": "image/aces",
+  "ez": "application/andrew-inset",
+  "fdf": "application/fdf",
+  "fdt": "application/fdt+xml",
+  "fits": "image/fits",
+  "g3": "image/g3fax",
+  "gbr": "application/rpki-ghostbusters",
+  "geojson": "application/geo+json",
+  "gif": "image/gif",
+  "glb": "model/gltf-binary",
+  "gltf": "model/gltf+json",
+  "gml": "application/gml+xml",
+  "gpx": "application/gpx+xml",
+  "gram": "application/srgs",
+  "grxml": "application/srgs+xml",
+  "gxf": "application/gxf",
+  "gz": "application/gzip",
+  "h261": "video/h261",
+  "h263": "video/h263",
+  "h264": "video/h264",
+  "heic": "image/heic",
+  "heics": "image/heic-sequence",
+  "heif": "image/heif",
+  "heifs": "image/heif-sequence",
+  "hej2": "image/hej2k",
+  "held": "application/atsc-held+xml",
+  "hjson": "application/hjson",
+  "hlp": "application/winhlp",
+  "hqx": "application/mac-binhex40",
+  "hsj2": "image/hsj2",
+  "htm": "text/html",
+  "html": "text/html",
+  "ics": "text/calendar",
+  "ief": "image/ief",
+  "ifb": "text/calendar",
+  "iges": "model/iges",
+  "igs": "model/iges",
+  "img": "application/octet-stream",
+  "in": "text/plain",
+  "ini": "text/plain",
+  "ink": "application/inkml+xml",
+  "inkml": "application/inkml+xml",
+  "ipfix": "application/ipfix",
+  "iso": "application/octet-stream",
+  "its": "application/its+xml",
+  "jade": "text/jade",
+  "jar": "application/java-archive",
+  "jhc": "image/jphc",
+  "jls": "image/jls",
+  "jp2": "image/jp2",
+  "jpe": "image/jpeg",
+  "jpeg": "image/jpeg",
+  "jpf": "image/jpx",
+  "jpg": "image/jpeg",
+  "jpg2": "image/jp2",
+  "jpgm": "image/jpm",
+  "jpgv": "video/jpeg",
+  "jph": "image/jph",
+  "jpm": "image/jpm",
+  "jpx": "image/jpx",
+  "js": "text/javascript",
+  "json": "application/json",
+  "json5": "application/json5",
+  "jsonld": "application/ld+json",
+  "jsonml": "application/jsonml+json",
+  "jsx": "text/jsx",
+  "jt": "model/jt",
+  "jxl": "image/jxl",
+  "jxr": "image/jxr",
+  "jxra": "image/jxra",
+  "jxrs": "image/jxrs",
+  "jxs": "image/jxs",
+  "jxsc": "image/jxsc",
+  "jxsi": "image/jxsi",
+  "jxss": "image/jxss",
+  "kar": "audio/midi",
+  "ktx": "image/ktx",
+  "ktx2": "image/ktx2",
+  "less": "text/less",
+  "lgr": "application/lgr+xml",
+  "list": "text/plain",
+  "litcoffee": "text/coffeescript",
+  "log": "text/plain",
+  "lostxml": "application/lost+xml",
+  "lrf": "application/octet-stream",
+  "m1v": "video/mpeg",
+  "m21": "application/mp21",
+  "m2a": "audio/mpeg",
+  "m2t": "video/mp2t",
+  "m2ts": "video/mp2t",
+  "m2v": "video/mpeg",
+  "m3a": "audio/mpeg",
+  "m4a": "audio/mp4",
+  "m4p": "application/mp4",
+  "m4s": "video/iso.segment",
+  "ma": "application/mathematica",
+  "mads": "application/mads+xml",
+  "maei": "application/mmt-aei+xml",
+  "man": "text/troff",
+  "manifest": "text/cache-manifest",
+  "map": "application/json",
+  "mar": "application/octet-stream",
+  "markdown": "text/markdown",
+  "mathml": "application/mathml+xml",
+  "mb": "application/mathematica",
+  "mbox": "application/mbox",
+  "md": "text/markdown",
+  "mdx": "text/mdx",
+  "me": "text/troff",
+  "mesh": "model/mesh",
+  "meta4": "application/metalink4+xml",
+  "metalink": "application/metalink+xml",
+  "mets": "application/mets+xml",
+  "mft": "application/rpki-manifest",
+  "mid": "audio/midi",
+  "midi": "audio/midi",
+  "mime": "message/rfc822",
+  "mj2": "video/mj2",
+  "mjp2": "video/mj2",
+  "mjs": "text/javascript",
+  "mml": "text/mathml",
+  "mods": "application/mods+xml",
+  "mov": "video/quicktime",
+  "mp2": "audio/mpeg",
+  "mp21": "application/mp21",
+  "mp2a": "audio/mpeg",
+  "mp3": "audio/mpeg",
+  "mp4": "video/mp4",
+  "mp4a": "audio/mp4",
+  "mp4s": "application/mp4",
+  "mp4v": "video/mp4",
+  "mpd": "application/dash+xml",
+  "mpe": "video/mpeg",
+  "mpeg": "video/mpeg",
+  "mpf": "application/media-policy-dataset+xml",
+  "mpg": "video/mpeg",
+  "mpg4": "video/mp4",
+  "mpga": "audio/mpeg",
+  "mpp": "application/dash-patch+xml",
+  "mrc": "application/marc",
+  "mrcx": "application/marcxml+xml",
+  "ms": "text/troff",
+  "mscml": "application/mediaservercontrol+xml",
+  "msh": "model/mesh",
+  "msi": "application/octet-stream",
+  "msix": "application/msix",
+  "msixbundle": "application/msixbundle",
+  "msm": "application/octet-stream",
+  "msp": "application/octet-stream",
+  "mtl": "model/mtl",
+  "mts": "video/mp2t",
+  "musd": "application/mmt-usd+xml",
+  "mxf": "application/mxf",
+  "mxmf": "audio/mobile-xmf",
+  "mxml": "application/xv+xml",
+  "n3": "text/n3",
+  "nb": "application/mathematica",
+  "nq": "application/n-quads",
+  "nt": "application/n-triples",
+  "obj": "model/obj",
+  "oda": "application/oda",
+  "oga": "audio/ogg",
+  "ogg": "audio/ogg",
+  "ogv": "video/ogg",
+  "ogx": "application/ogg",
+  "omdoc": "application/omdoc+xml",
+  "onepkg": "application/onenote",
+  "onetmp": "application/onenote",
+  "onetoc": "application/onenote",
+  "onetoc2": "application/onenote",
+  "opf": "application/oebps-package+xml",
+  "opus": "audio/ogg",
+  "otf": "font/otf",
+  "owl": "application/rdf+xml",
+  "oxps": "application/oxps",
+  "p10": "application/pkcs10",
+  "p7c": "application/pkcs7-mime",
+  "p7m": "application/pkcs7-mime",
+  "p7s": "application/pkcs7-signature",
+  "p8": "application/pkcs8",
+  "pdf": "application/pdf",
+  "pfr": "application/font-tdpfr",
+  "pgp": "application/pgp-encrypted",
+  "pkg": "application/octet-stream",
+  "pki": "application/pkixcmp",
+  "pkipath": "application/pkix-pkipath",
+  "pls": "application/pls+xml",
+  "png": "image/png",
+  "prc": "model/prc",
+  "prf": "application/pics-rules",
+  "provx": "application/provenance+xml",
+  "ps": "application/postscript",
+  "pskcxml": "application/pskc+xml",
+  "pti": "image/prs.pti",
+  "qt": "video/quicktime",
+  "raml": "application/raml+yaml",
+  "rapd": "application/route-apd+xml",
+  "rdf": "application/rdf+xml",
+  "relo": "application/p2p-overlay+xml",
+  "rif": "application/reginfo+xml",
+  "rl": "application/resource-lists+xml",
+  "rld": "application/resource-lists-diff+xml",
+  "rmi": "audio/midi",
+  "rnc": "application/relax-ng-compact-syntax",
+  "rng": "application/xml",
+  "roa": "application/rpki-roa",
+  "roff": "text/troff",
+  "rq": "application/sparql-query",
+  "rs": "application/rls-services+xml",
+  "rsat": "application/atsc-rsat+xml",
+  "rsd": "application/rsd+xml",
+  "rsheet": "application/urc-ressheet+xml",
+  "rss": "application/rss+xml",
+  "rtf": "text/rtf",
+  "rtx": "text/richtext",
+  "rusd": "application/route-usd+xml",
+  "s3m": "audio/s3m",
+  "sbml": "application/sbml+xml",
+  "scq": "application/scvp-cv-request",
+  "scs": "application/scvp-cv-response",
+  "sdp": "application/sdp",
+  "senmlx": "application/senml+xml",
+  "sensmlx": "application/sensml+xml",
+  "ser": "application/java-serialized-object",
+  "setpay": "application/set-payment-initiation",
+  "setreg": "application/set-registration-initiation",
+  "sgi": "image/sgi",
+  "sgm": "text/sgml",
+  "sgml": "text/sgml",
+  "shex": "text/shex",
+  "shf": "application/shf+xml",
+  "shtml": "text/html",
+  "sieve": "application/sieve",
+  "sig": "application/pgp-signature",
+  "sil": "audio/silk",
+  "silo": "model/mesh",
+  "siv": "application/sieve",
+  "slim": "text/slim",
+  "slm": "text/slim",
+  "sls": "application/route-s-tsid+xml",
+  "smi": "application/smil+xml",
+  "smil": "application/smil+xml",
+  "snd": "audio/basic",
+  "so": "application/octet-stream",
+  "spdx": "text/spdx",
+  "spp": "application/scvp-vp-response",
+  "spq": "application/scvp-vp-request",
+  "spx": "audio/ogg",
+  "sql": "application/sql",
+  "sru": "application/sru+xml",
+  "srx": "application/sparql-results+xml",
+  "ssdl": "application/ssdl+xml",
+  "ssml": "application/ssml+xml",
+  "stk": "application/hyperstudio",
+  "stl": "model/stl",
+  "stpx": "model/step+xml",
+  "stpxz": "model/step-xml+zip",
+  "stpz": "model/step+zip",
+  "styl": "text/stylus",
+  "stylus": "text/stylus",
+  "svg": "image/svg+xml",
+  "svgz": "image/svg+xml",
+  "swidtag": "application/swid+xml",
+  "t": "text/troff",
+  "t38": "image/t38",
+  "td": "application/urc-targetdesc+xml",
+  "tei": "application/tei+xml",
+  "teicorpus": "application/tei+xml",
+  "text": "text/plain",
+  "tfi": "application/thraud+xml",
+  "tfx": "image/tiff-fx",
+  "tif": "image/tiff",
+  "tiff": "image/tiff",
+  "toml": "application/toml",
+  "tr": "text/troff",
+  "trig": "application/trig",
+  "ts": "video/mp2t",
+  "tsd": "application/timestamped-data",
+  "tsv": "text/tab-separated-values",
+  "ttc": "font/collection",
+  "ttf": "font/ttf",
+  "ttl": "text/turtle",
+  "ttml": "application/ttml+xml",
+  "txt": "text/plain",
+  "u3d": "model/u3d",
+  "u8dsn": "message/global-delivery-status",
+  "u8hdr": "message/global-headers",
+  "u8mdn": "message/global-disposition-notification",
+  "u8msg": "message/global",
+  "ubj": "application/ubjson",
+  "uri": "text/uri-list",
+  "uris": "text/uri-list",
+  "urls": "text/uri-list",
+  "vcard": "text/vcard",
+  "vrml": "model/vrml",
+  "vtt": "text/vtt",
+  "vxml": "application/voicexml+xml",
+  "war": "application/java-archive",
+  "wasm": "application/wasm",
+  "wav": "audio/wav",
+  "weba": "audio/webm",
+  "webm": "video/webm",
+  "webmanifest": "application/manifest+json",
+  "webp": "image/webp",
+  "wgsl": "text/wgsl",
+  "wgt": "application/widget",
+  "wif": "application/watcherinfo+xml",
+  "wmf": "image/wmf",
+  "woff": "font/woff",
+  "woff2": "font/woff2",
+  "wrl": "model/vrml",
+  "wsdl": "application/wsdl+xml",
+  "wspolicy": "application/wspolicy+xml",
+  "x3d": "model/x3d+xml",
+  "x3db": "model/x3d+fastinfoset",
+  "x3dbz": "model/x3d+binary",
+  "x3dv": "model/x3d-vrml",
+  "x3dvz": "model/x3d+vrml",
+  "x3dz": "model/x3d+xml",
+  "xaml": "application/xaml+xml",
+  "xav": "application/xcap-att+xml",
+  "xca": "application/xcap-caps+xml",
+  "xcs": "application/calendar+xml",
+  "xdf": "application/xcap-diff+xml",
+  "xdssc": "application/dssc+xml",
+  "xel": "application/xcap-el+xml",
+  "xenc": "application/xenc+xml",
+  "xer": "application/patch-ops-error+xml",
+  "xfdf": "application/xfdf",
+  "xht": "application/xhtml+xml",
+  "xhtml": "application/xhtml+xml",
+  "xhvml": "application/xv+xml",
+  "xlf": "application/xliff+xml",
+  "xm": "audio/xm",
+  "xml": "text/xml",
+  "xns": "application/xcap-ns+xml",
+  "xop": "application/xop+xml",
+  "xpl": "application/xproc+xml",
+  "xsd": "application/xml",
+  "xsf": "application/prs.xsf+xml",
+  "xsl": "application/xml",
+  "xslt": "application/xml",
+  "xspf": "application/xspf+xml",
+  "xvm": "application/xv+xml",
+  "xvml": "application/xv+xml",
+  "yaml": "text/yaml",
+  "yang": "application/yang",
+  "yin": "application/yin+xml",
+  "yml": "text/yaml",
+  "zip": "application/zip"
+};
+
+function lookup(extn) {
+	let tmp = ('' + extn).trim().toLowerCase();
+	let idx = tmp.lastIndexOf('.');
+	return mimes[!~idx ? tmp : tmp.substring(++idx)];
+}
+
+const $$Astro$1 = createAstro();
+const $$Picture = createComponent(async ($$result, $$props, $$slots) => {
+  const Astro2 = $$result.createAstro($$Astro$1, $$props, $$slots);
+  Astro2.self = $$Picture;
+  const defaultFormats = ["webp"];
+  const defaultFallbackFormat = "png";
+  const specialFormatsFallback = ["gif", "svg", "jpg", "jpeg"];
+  const { formats = defaultFormats, pictureAttributes = {}, fallbackFormat, ...props } = Astro2.props;
+  if (props.alt === void 0 || props.alt === null) {
+    throw new AstroError(ImageMissingAlt);
+  }
+  const scopedStyleClass = props.class?.match(/\bastro-\w{8}\b/)?.[0];
+  if (scopedStyleClass) {
+    if (pictureAttributes.class) {
+      pictureAttributes.class = `${pictureAttributes.class} ${scopedStyleClass}`;
+    } else {
+      pictureAttributes.class = scopedStyleClass;
+    }
+  }
+  const layout = props.layout ?? imageConfig.layout ?? "none";
+  const useResponsive = layout !== "none";
+  if (useResponsive) {
+    props.layout ??= imageConfig.layout;
+    props.fit ??= imageConfig.objectFit ?? "cover";
+    props.position ??= imageConfig.objectPosition ?? "center";
+  }
+  for (const key in props) {
+    if (key.startsWith("data-astro-cid")) {
+      pictureAttributes[key] = props[key];
+    }
+  }
+  const originalSrc = await resolveSrc(props.src);
+  const optimizedImages = await Promise.all(
+    formats.map(
+      async (format) => await getImage({
+        ...props,
+        src: originalSrc,
+        format,
+        widths: props.widths,
+        densities: props.densities
+      })
+    )
+  );
+  let resultFallbackFormat = fallbackFormat ?? defaultFallbackFormat;
+  if (!fallbackFormat && isESMImportedImage(originalSrc) && specialFormatsFallback.includes(originalSrc.format)) {
+    resultFallbackFormat = originalSrc.format;
+  }
+  const fallbackImage = await getImage({
+    ...props,
+    format: resultFallbackFormat,
+    widths: props.widths,
+    densities: props.densities
+  });
+  const imgAdditionalAttributes = {};
+  const sourceAdditionalAttributes = {};
+  if (props.sizes) {
+    sourceAdditionalAttributes.sizes = props.sizes;
+  }
+  if (fallbackImage.srcSet.values.length > 0) {
+    imgAdditionalAttributes.srcset = fallbackImage.srcSet.attribute;
+  }
+  const { class: className, ...attributes } = {
+    ...imgAdditionalAttributes,
+    ...fallbackImage.attributes
+  };
+  return renderTemplate`${maybeRenderHead()}<picture${spreadAttributes(pictureAttributes)}> ${Object.entries(optimizedImages).map(([_, image]) => {
+    const srcsetAttribute = props.densities || !props.densities && !props.widths && !useResponsive ? `${image.src}${image.srcSet.values.length > 0 ? ", " + image.srcSet.attribute : ""}` : image.srcSet.attribute;
+    return renderTemplate`<source${addAttribute(srcsetAttribute, "srcset")}${addAttribute(lookup(image.options.format ?? image.src) ?? `image/${image.options.format}`, "type")}${spreadAttributes(sourceAdditionalAttributes)}>`;
+  })}  <img${addAttribute(fallbackImage.src, "src")}${spreadAttributes(attributes)}${addAttribute(className, "class")}> </picture>`;
+}, "/home/fc/Projects/glance/node_modules/astro/components/Picture.astro", void 0);
+
+const fontsMod = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+  __proto__: null
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const $$Astro = createAstro();
+const $$Font = createComponent(($$result, $$props, $$slots) => {
+  const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
+  Astro2.self = $$Font;
+  const { internalConsumableMap } = fontsMod;
+  if (!internalConsumableMap) {
+    throw new AstroError(ExperimentalFontsNotEnabled);
+  }
+  const { cssVariable, preload = false } = Astro2.props;
+  const data = internalConsumableMap.get(cssVariable);
+  if (!data) {
+    throw new AstroError({
+      ...FontFamilyNotFound,
+      message: FontFamilyNotFound.message(cssVariable)
+    });
+  }
+  return renderTemplate`${preload && data.preloadData.map(({ url, type }) => renderTemplate`<link rel="preload"${addAttribute(url, "href")} as="font"${addAttribute(`font/${type}`, "type")} crossorigin>`)}<style>${unescapeHTML(data.css)}</style>`;
+}, "/home/fc/Projects/glance/node_modules/astro/components/Font.astro", void 0);
+
+const imageConfig = {"endpoint":{"route":"/_image","entrypoint":"@astrojs/cloudflare/image-endpoint"},"service":{"entrypoint":"astro/assets/services/sharp","config":{}},"domains":[],"remotePatterns":[],"responsiveStyles":false};
+							const getImage = async (options) => await getImage$1(options, imageConfig);
+
+const prerender = false;
+const GET = (ctx) => {
+  const href = ctx.url.searchParams.get("href");
+  if (!href) {
+    return new Response("Missing 'href' query parameter", {
+      status: 400,
+      statusText: "Missing 'href' query parameter"
+    });
+  }
+  if (isRemotePath(href)) {
+    if (isRemoteAllowed(href, imageConfig) === false) {
+      return new Response("Forbidden", { status: 403 });
+    } else {
+      return Response.redirect(href, 302);
+    }
+  }
+  const proxied = new URL(href, ctx.url.origin);
+  if (proxied.origin !== ctx.url.origin) {
+    return new Response("Forbidden", { status: 403 });
+  }
+  return fetch(proxied);
+};
+
+const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+  __proto__: null,
+  GET,
+  prerender
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const page = () => _page;
+
+export { page as a, baseService as b, parseQuality as p };

+ 3164 - 0
dist/_worker.js/chunks/index_BfW_FHpJ.mjs

@@ -0,0 +1,3164 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { K as decryptString, O as createSlotValueFromString, P as isAstroComponentFactory, k as renderComponent, r as renderTemplate, q as REROUTE_DIRECTIVE_HEADER, A as AstroError, Q as i18nNoLocaleFoundInPath, S as ResponseSentError, T as originPathnameSymbol, V as RewriteWithBodyUsed, W as GetStaticPathsRequired, X as InvalidGetStaticPathsReturn, Y as InvalidGetStaticPathsEntry, Z as GetStaticPathsExpectedParams, _ as GetStaticPathsInvalidRouteParam, $ as PageNumberParamNotFound, D as DEFAULT_404_COMPONENT, a0 as NoMatchingStaticPathFound, a1 as PrerenderDynamicEndpointPathCollide, a2 as ReservedSlotName, a3 as renderSlotToString, a4 as renderJSX, a5 as chunkToString, a6 as isRenderInstruction, a7 as MiddlewareNoDataOrNextCalled, a8 as MiddlewareNotAResponse, a9 as SessionStorageInitError, aa as SessionStorageSaveError, R as ROUTE_TYPE_HEADER, ab as ForbiddenRewrite, ac as ASTRO_VERSION, ad as CspNotEnabled, ae as green, af as LocalsReassigned, ag as generateCspDigest, ah as PrerenderClientAddressNotAvailable, C as clientAddressSymbol, ai as ClientAddressNotAvailable, aj as StaticClientAddressNotAvailable, ak as AstroResponseHeadersReassigned, J as responseSentSymbol$1, al as renderPage, am as REWRITE_DIRECTIVE_HEADER_KEY, an as REWRITE_DIRECTIVE_HEADER_VALUE, ao as renderEndpoint } from './astro/server_WO3f6Mge.mjs';
+import { g as getActionQueryString, a as deserializeActionResult, b as distExports, D as DEFAULT_404_ROUTE, A as ActionError, s as serializeActionResult, c as ACTION_RPC_ROUTE_PATTERN, f as ACTION_QUERY_PARAMS, u as unflatten$1, h as stringify$2 } from './astro-designed-error-pages_CuMapJD2.mjs';
+import { a as appendForwardSlash, j as joinPaths, r as removeTrailingForwardSlash, p as prependForwardSlash, t as trimSlashes } from './path_CH3auf61.mjs';
+
+const ACTION_API_CONTEXT_SYMBOL = Symbol.for("astro.actionAPIContext");
+const formContentTypes = ["application/x-www-form-urlencoded", "multipart/form-data"];
+function hasContentType(contentType, expected) {
+  const type = contentType.split(";")[0].toLowerCase();
+  return expected.some((t) => type === t);
+}
+
+function hasActionPayload(locals) {
+  return "_actionPayload" in locals;
+}
+function createGetActionResult(locals) {
+  return (actionFn) => {
+    if (!hasActionPayload(locals) || actionFn.toString() !== getActionQueryString(locals._actionPayload.actionName)) {
+      return void 0;
+    }
+    return deserializeActionResult(locals._actionPayload.actionResult);
+  };
+}
+function createCallAction(context) {
+  return (baseAction, input) => {
+    Reflect.set(context, ACTION_API_CONTEXT_SYMBOL, true);
+    const action = baseAction.bind(context);
+    return action(input);
+  };
+}
+
+function shouldAppendForwardSlash(trailingSlash, buildFormat) {
+  switch (trailingSlash) {
+    case "always":
+      return true;
+    case "never":
+      return false;
+    case "ignore": {
+      switch (buildFormat) {
+        case "directory":
+          return true;
+        case "preserve":
+        case "file":
+          return false;
+      }
+    }
+  }
+}
+
+function redirectIsExternal(redirect) {
+  if (typeof redirect === "string") {
+    return redirect.startsWith("http://") || redirect.startsWith("https://");
+  } else {
+    return redirect.destination.startsWith("http://") || redirect.destination.startsWith("https://");
+  }
+}
+async function renderRedirect(renderContext) {
+  const {
+    request: { method },
+    routeData
+  } = renderContext;
+  const { redirect, redirectRoute } = routeData;
+  const status = redirectRoute && typeof redirect === "object" ? redirect.status : method === "GET" ? 301 : 308;
+  const headers = { location: encodeURI(redirectRouteGenerate(renderContext)) };
+  if (redirect && redirectIsExternal(redirect)) {
+    if (typeof redirect === "string") {
+      return Response.redirect(redirect, status);
+    } else {
+      return Response.redirect(redirect.destination, status);
+    }
+  }
+  return new Response(null, { status, headers });
+}
+function redirectRouteGenerate(renderContext) {
+  const {
+    params,
+    routeData: { redirect, redirectRoute }
+  } = renderContext;
+  if (typeof redirectRoute !== "undefined") {
+    return redirectRoute?.generate(params) || redirectRoute?.pathname || "/";
+  } else if (typeof redirect === "string") {
+    if (redirectIsExternal(redirect)) {
+      return redirect;
+    } else {
+      let target = redirect;
+      for (const param of Object.keys(params)) {
+        const paramValue = params[param];
+        target = target.replace(`[${param}]`, paramValue).replace(`[...${param}]`, paramValue);
+      }
+      return target;
+    }
+  } else if (typeof redirect === "undefined") {
+    return "/";
+  }
+  return redirect.destination;
+}
+
+const SERVER_ISLAND_ROUTE = "/_server-islands/[name]";
+const SERVER_ISLAND_COMPONENT = "_server-islands.astro";
+const SERVER_ISLAND_BASE_PREFIX = "_server-islands";
+function badRequest(reason) {
+  return new Response(null, {
+    status: 400,
+    statusText: "Bad request: " + reason
+  });
+}
+async function getRequestData(request) {
+  switch (request.method) {
+    case "GET": {
+      const url = new URL(request.url);
+      const params = url.searchParams;
+      if (!params.has("s") || !params.has("e") || !params.has("p")) {
+        return badRequest("Missing required query parameters.");
+      }
+      const rawSlots = params.get("s");
+      try {
+        return {
+          componentExport: params.get("e"),
+          encryptedProps: params.get("p"),
+          slots: JSON.parse(rawSlots)
+        };
+      } catch {
+        return badRequest("Invalid slots format.");
+      }
+    }
+    case "POST": {
+      try {
+        const raw = await request.text();
+        const data = JSON.parse(raw);
+        return data;
+      } catch {
+        return badRequest("Request format is invalid.");
+      }
+    }
+    default: {
+      return new Response(null, { status: 405 });
+    }
+  }
+}
+function createEndpoint(manifest) {
+  const page = async (result) => {
+    const params = result.params;
+    if (!params.name) {
+      return new Response(null, {
+        status: 400,
+        statusText: "Bad request"
+      });
+    }
+    const componentId = params.name;
+    const data = await getRequestData(result.request);
+    if (data instanceof Response) {
+      return data;
+    }
+    const imp = manifest.serverIslandMap?.get(componentId);
+    if (!imp) {
+      return new Response(null, {
+        status: 404,
+        statusText: "Not found"
+      });
+    }
+    const key = await manifest.key;
+    const encryptedProps = data.encryptedProps;
+    const propString = encryptedProps === "" ? "{}" : await decryptString(key, encryptedProps);
+    const props = JSON.parse(propString);
+    const componentModule = await imp();
+    let Component = componentModule[data.componentExport];
+    const slots = {};
+    for (const prop in data.slots) {
+      slots[prop] = createSlotValueFromString(data.slots[prop]);
+    }
+    result.response.headers.set("X-Robots-Tag", "noindex");
+    if (isAstroComponentFactory(Component)) {
+      const ServerIsland = Component;
+      Component = function(...args) {
+        return ServerIsland.apply(this, args);
+      };
+      Object.assign(Component, ServerIsland);
+      Component.propagation = "self";
+    }
+    return renderTemplate`${renderComponent(result, "Component", Component, props, slots)}`;
+  };
+  page.isAstroComponentFactory = true;
+  const instance = {
+    default: page,
+    partial: true
+  };
+  return instance;
+}
+
+function matchRoute(pathname, manifest) {
+  return manifest.routes.find((route) => {
+    return route.pattern.test(pathname) || route.fallbackRoutes.some((fallbackRoute) => fallbackRoute.pattern.test(pathname));
+  });
+}
+const ROUTE404_RE = /^\/404\/?$/;
+const ROUTE500_RE = /^\/500\/?$/;
+function isRoute404(route) {
+  return ROUTE404_RE.test(route);
+}
+function isRoute500(route) {
+  return ROUTE500_RE.test(route);
+}
+function isRoute404or500(route) {
+  return isRoute404(route.route) || isRoute500(route.route);
+}
+function isRouteServerIsland(route) {
+  return route.component === SERVER_ISLAND_COMPONENT;
+}
+function isRequestServerIsland(request, base = "") {
+  const url = new URL(request.url);
+  const pathname = base === "/" ? url.pathname.slice(base.length) : url.pathname.slice(base.length + 1);
+  return pathname.startsWith(SERVER_ISLAND_BASE_PREFIX);
+}
+function requestIs404Or500(request, base = "") {
+  const url = new URL(request.url);
+  const pathname = url.pathname.slice(base.length);
+  return isRoute404(pathname) || isRoute500(pathname);
+}
+function isRouteExternalRedirect(route) {
+  return !!(route.type === "redirect" && route.redirect && redirectIsExternal(route.redirect));
+}
+
+function requestHasLocale(locales) {
+  return function(context) {
+    return pathHasLocale(context.url.pathname, locales);
+  };
+}
+function pathHasLocale(path, locales) {
+  const segments = path.split("/").map(normalizeThePath);
+  for (const segment of segments) {
+    for (const locale of locales) {
+      if (typeof locale === "string") {
+        if (normalizeTheLocale(segment) === normalizeTheLocale(locale)) {
+          return true;
+        }
+      } else if (segment === locale.path) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+function getPathByLocale(locale, locales) {
+  for (const loopLocale of locales) {
+    if (typeof loopLocale === "string") {
+      if (loopLocale === locale) {
+        return loopLocale;
+      }
+    } else {
+      for (const code of loopLocale.codes) {
+        if (code === locale) {
+          return loopLocale.path;
+        }
+      }
+    }
+  }
+  throw new AstroError(i18nNoLocaleFoundInPath);
+}
+function normalizeTheLocale(locale) {
+  return locale.replaceAll("_", "-").toLowerCase();
+}
+function normalizeThePath(path) {
+  return path.endsWith(".html") ? path.slice(0, -5) : path;
+}
+function getAllCodes(locales) {
+  const result = [];
+  for (const loopLocale of locales) {
+    if (typeof loopLocale === "string") {
+      result.push(loopLocale);
+    } else {
+      result.push(...loopLocale.codes);
+    }
+  }
+  return result;
+}
+function redirectToDefaultLocale({
+  trailingSlash,
+  format,
+  base,
+  defaultLocale
+}) {
+  return function(context, statusCode) {
+    if (shouldAppendForwardSlash(trailingSlash, format)) {
+      return context.redirect(`${appendForwardSlash(joinPaths(base, defaultLocale))}`, statusCode);
+    } else {
+      return context.redirect(`${joinPaths(base, defaultLocale)}`, statusCode);
+    }
+  };
+}
+function notFound({ base, locales, fallback }) {
+  return function(context, response) {
+    if (response?.headers.get(REROUTE_DIRECTIVE_HEADER) === "no" && typeof fallback === "undefined") {
+      return response;
+    }
+    const url = context.url;
+    const isRoot = url.pathname === base + "/" || url.pathname === base;
+    if (!(isRoot || pathHasLocale(url.pathname, locales))) {
+      if (response) {
+        response.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
+        return new Response(response.body, {
+          status: 404,
+          headers: response.headers
+        });
+      } else {
+        return new Response(null, {
+          status: 404,
+          headers: {
+            [REROUTE_DIRECTIVE_HEADER]: "no"
+          }
+        });
+      }
+    }
+    return void 0;
+  };
+}
+function redirectToFallback({
+  fallback,
+  locales,
+  defaultLocale,
+  strategy,
+  base,
+  fallbackType
+}) {
+  return async function(context, response) {
+    if (response.status >= 300 && fallback) {
+      const fallbackKeys = fallback ? Object.keys(fallback) : [];
+      const segments = context.url.pathname.split("/");
+      const urlLocale = segments.find((segment) => {
+        for (const locale of locales) {
+          if (typeof locale === "string") {
+            if (locale === segment) {
+              return true;
+            }
+          } else if (locale.path === segment) {
+            return true;
+          }
+        }
+        return false;
+      });
+      if (urlLocale && fallbackKeys.includes(urlLocale)) {
+        const fallbackLocale = fallback[urlLocale];
+        const pathFallbackLocale = getPathByLocale(fallbackLocale, locales);
+        let newPathname;
+        if (pathFallbackLocale === defaultLocale && strategy === "pathname-prefix-other-locales") {
+          if (context.url.pathname.includes(`${base}`)) {
+            newPathname = context.url.pathname.replace(`/${urlLocale}`, ``);
+            if (newPathname === "") {
+              newPathname = "/";
+            }
+          } else {
+            newPathname = context.url.pathname.replace(`/${urlLocale}`, `/`);
+          }
+        } else {
+          newPathname = context.url.pathname.replace(`/${urlLocale}`, `/${pathFallbackLocale}`);
+        }
+        if (fallbackType === "rewrite") {
+          return await context.rewrite(newPathname + context.url.search);
+        } else {
+          return context.redirect(newPathname + context.url.search);
+        }
+      }
+    }
+    return response;
+  };
+}
+
+function parseLocale(header) {
+  if (header === "*") {
+    return [{ locale: header, qualityValue: void 0 }];
+  }
+  const result = [];
+  const localeValues = header.split(",").map((str) => str.trim());
+  for (const localeValue of localeValues) {
+    const split = localeValue.split(";").map((str) => str.trim());
+    const localeName = split[0];
+    const qualityValue = split[1];
+    if (!split) {
+      continue;
+    }
+    if (qualityValue && qualityValue.startsWith("q=")) {
+      const qualityValueAsFloat = Number.parseFloat(qualityValue.slice("q=".length));
+      if (Number.isNaN(qualityValueAsFloat) || qualityValueAsFloat > 1) {
+        result.push({
+          locale: localeName,
+          qualityValue: void 0
+        });
+      } else {
+        result.push({
+          locale: localeName,
+          qualityValue: qualityValueAsFloat
+        });
+      }
+    } else {
+      result.push({
+        locale: localeName,
+        qualityValue: void 0
+      });
+    }
+  }
+  return result;
+}
+function sortAndFilterLocales(browserLocaleList, locales) {
+  const normalizedLocales = getAllCodes(locales).map(normalizeTheLocale);
+  return browserLocaleList.filter((browserLocale) => {
+    if (browserLocale.locale !== "*") {
+      return normalizedLocales.includes(normalizeTheLocale(browserLocale.locale));
+    }
+    return true;
+  }).sort((a, b) => {
+    if (a.qualityValue && b.qualityValue) {
+      return Math.sign(b.qualityValue - a.qualityValue);
+    }
+    return 0;
+  });
+}
+function computePreferredLocale(request, locales) {
+  const acceptHeader = request.headers.get("Accept-Language");
+  let result = void 0;
+  if (acceptHeader) {
+    const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
+    const firstResult = browserLocaleList.at(0);
+    if (firstResult && firstResult.locale !== "*") {
+      for (const currentLocale of locales) {
+        if (typeof currentLocale === "string") {
+          if (normalizeTheLocale(currentLocale) === normalizeTheLocale(firstResult.locale)) {
+            result = currentLocale;
+            break;
+          }
+        } else {
+          for (const currentCode of currentLocale.codes) {
+            if (normalizeTheLocale(currentCode) === normalizeTheLocale(firstResult.locale)) {
+              result = currentCode;
+              break;
+            }
+          }
+        }
+      }
+    }
+  }
+  return result;
+}
+function computePreferredLocaleList(request, locales) {
+  const acceptHeader = request.headers.get("Accept-Language");
+  let result = [];
+  if (acceptHeader) {
+    const browserLocaleList = sortAndFilterLocales(parseLocale(acceptHeader), locales);
+    if (browserLocaleList.length === 1 && browserLocaleList.at(0).locale === "*") {
+      return getAllCodes(locales);
+    } else if (browserLocaleList.length > 0) {
+      for (const browserLocale of browserLocaleList) {
+        for (const loopLocale of locales) {
+          if (typeof loopLocale === "string") {
+            if (normalizeTheLocale(loopLocale) === normalizeTheLocale(browserLocale.locale)) {
+              result.push(loopLocale);
+            }
+          } else {
+            for (const code of loopLocale.codes) {
+              if (code === browserLocale.locale) {
+                result.push(code);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  return result;
+}
+function computeCurrentLocale(pathname, locales, defaultLocale) {
+  for (const segment of pathname.split("/").map(normalizeThePath)) {
+    for (const locale of locales) {
+      if (typeof locale === "string") {
+        if (!segment.includes(locale)) continue;
+        if (normalizeTheLocale(locale) === normalizeTheLocale(segment)) {
+          return locale;
+        }
+      } else {
+        if (locale.path === segment) {
+          return locale.codes.at(0);
+        } else {
+          for (const code of locale.codes) {
+            if (normalizeTheLocale(code) === normalizeTheLocale(segment)) {
+              return code;
+            }
+          }
+        }
+      }
+    }
+  }
+  for (const locale of locales) {
+    if (typeof locale === "string") {
+      if (locale === defaultLocale) {
+        return locale;
+      }
+    } else {
+      if (locale.path === defaultLocale) {
+        return locale.codes.at(0);
+      }
+    }
+  }
+}
+
+const DELETED_EXPIRATION = /* @__PURE__ */ new Date(0);
+const DELETED_VALUE = "deleted";
+const responseSentSymbol = Symbol.for("astro.responseSent");
+const identity = (value) => value;
+class AstroCookie {
+  constructor(value) {
+    this.value = value;
+  }
+  json() {
+    if (this.value === void 0) {
+      throw new Error(`Cannot convert undefined to an object.`);
+    }
+    return JSON.parse(this.value);
+  }
+  number() {
+    return Number(this.value);
+  }
+  boolean() {
+    if (this.value === "false") return false;
+    if (this.value === "0") return false;
+    return Boolean(this.value);
+  }
+}
+class AstroCookies {
+  #request;
+  #requestValues;
+  #outgoing;
+  #consumed;
+  constructor(request) {
+    this.#request = request;
+    this.#requestValues = null;
+    this.#outgoing = null;
+    this.#consumed = false;
+  }
+  /**
+   * Astro.cookies.delete(key) is used to delete a cookie. Using this method will result
+   * in a Set-Cookie header added to the response.
+   * @param key The cookie to delete
+   * @param options Options related to this deletion, such as the path of the cookie.
+   */
+  delete(key, options) {
+    const {
+      // @ts-expect-error
+      maxAge: _ignoredMaxAge,
+      // @ts-expect-error
+      expires: _ignoredExpires,
+      ...sanitizedOptions
+    } = options || {};
+    const serializeOptions = {
+      expires: DELETED_EXPIRATION,
+      ...sanitizedOptions
+    };
+    this.#ensureOutgoingMap().set(key, [
+      DELETED_VALUE,
+      distExports.serialize(key, DELETED_VALUE, serializeOptions),
+      false
+    ]);
+  }
+  /**
+   * Astro.cookies.get(key) is used to get a cookie value. The cookie value is read from the
+   * request. If you have set a cookie via Astro.cookies.set(key, value), the value will be taken
+   * from that set call, overriding any values already part of the request.
+   * @param key The cookie to get.
+   * @returns An object containing the cookie value as well as convenience methods for converting its value.
+   */
+  get(key, options = void 0) {
+    if (this.#outgoing?.has(key)) {
+      let [serializedValue, , isSetValue] = this.#outgoing.get(key);
+      if (isSetValue) {
+        return new AstroCookie(serializedValue);
+      } else {
+        return void 0;
+      }
+    }
+    const decode = options?.decode ?? decodeURIComponent;
+    const values = this.#ensureParsed();
+    if (key in values) {
+      const value = values[key];
+      if (value) {
+        return new AstroCookie(decode(value));
+      }
+    }
+  }
+  /**
+   * Astro.cookies.has(key) returns a boolean indicating whether this cookie is either
+   * part of the initial request or set via Astro.cookies.set(key)
+   * @param key The cookie to check for.
+   * @param _options This parameter is no longer used.
+   * @returns
+   */
+  has(key, _options) {
+    if (this.#outgoing?.has(key)) {
+      let [, , isSetValue] = this.#outgoing.get(key);
+      return isSetValue;
+    }
+    const values = this.#ensureParsed();
+    return values[key] !== void 0;
+  }
+  /**
+   * Astro.cookies.set(key, value) is used to set a cookie's value. If provided
+   * an object it will be stringified via JSON.stringify(value). Additionally you
+   * can provide options customizing how this cookie will be set, such as setting httpOnly
+   * in order to prevent the cookie from being read in client-side JavaScript.
+   * @param key The name of the cookie to set.
+   * @param value A value, either a string or other primitive or an object.
+   * @param options Options for the cookie, such as the path and security settings.
+   */
+  set(key, value, options) {
+    if (this.#consumed) {
+      const warning = new Error(
+        "Astro.cookies.set() was called after the cookies had already been sent to the browser.\nThis may have happened if this method was called in an imported component.\nPlease make sure that Astro.cookies.set() is only called in the frontmatter of the main page."
+      );
+      warning.name = "Warning";
+      console.warn(warning);
+    }
+    let serializedValue;
+    if (typeof value === "string") {
+      serializedValue = value;
+    } else {
+      let toStringValue = value.toString();
+      if (toStringValue === Object.prototype.toString.call(value)) {
+        serializedValue = JSON.stringify(value);
+      } else {
+        serializedValue = toStringValue;
+      }
+    }
+    const serializeOptions = {};
+    if (options) {
+      Object.assign(serializeOptions, options);
+    }
+    this.#ensureOutgoingMap().set(key, [
+      serializedValue,
+      distExports.serialize(key, serializedValue, serializeOptions),
+      true
+    ]);
+    if (this.#request[responseSentSymbol]) {
+      throw new AstroError({
+        ...ResponseSentError
+      });
+    }
+  }
+  /**
+   * Merges a new AstroCookies instance into the current instance. Any new cookies
+   * will be added to the current instance, overwriting any existing cookies with the same name.
+   */
+  merge(cookies) {
+    const outgoing = cookies.#outgoing;
+    if (outgoing) {
+      for (const [key, value] of outgoing) {
+        this.#ensureOutgoingMap().set(key, value);
+      }
+    }
+  }
+  /**
+   * Astro.cookies.header() returns an iterator for the cookies that have previously
+   * been set by either Astro.cookies.set() or Astro.cookies.delete().
+   * This method is primarily used by adapters to set the header on outgoing responses.
+   * @returns
+   */
+  *headers() {
+    if (this.#outgoing == null) return;
+    for (const [, value] of this.#outgoing) {
+      yield value[1];
+    }
+  }
+  /**
+   * Behaves the same as AstroCookies.prototype.headers(),
+   * but allows a warning when cookies are set after the instance is consumed.
+   */
+  static consume(cookies) {
+    cookies.#consumed = true;
+    return cookies.headers();
+  }
+  #ensureParsed() {
+    if (!this.#requestValues) {
+      this.#parse();
+    }
+    if (!this.#requestValues) {
+      this.#requestValues = {};
+    }
+    return this.#requestValues;
+  }
+  #ensureOutgoingMap() {
+    if (!this.#outgoing) {
+      this.#outgoing = /* @__PURE__ */ new Map();
+    }
+    return this.#outgoing;
+  }
+  #parse() {
+    const raw = this.#request.headers.get("cookie");
+    if (!raw) {
+      return;
+    }
+    this.#requestValues = distExports.parse(raw, { decode: identity });
+  }
+}
+
+const astroCookiesSymbol = Symbol.for("astro.cookies");
+function attachCookiesToResponse(response, cookies) {
+  Reflect.set(response, astroCookiesSymbol, cookies);
+}
+function getCookiesFromResponse(response) {
+  let cookies = Reflect.get(response, astroCookiesSymbol);
+  if (cookies != null) {
+    return cookies;
+  } else {
+    return void 0;
+  }
+}
+function* getSetCookiesFromResponse(response) {
+  const cookies = getCookiesFromResponse(response);
+  if (!cookies) {
+    return [];
+  }
+  for (const headerValue of AstroCookies.consume(cookies)) {
+    yield headerValue;
+  }
+  return [];
+}
+
+function createRequest({
+  url,
+  headers,
+  method = "GET",
+  body = void 0,
+  logger,
+  isPrerendered = false,
+  routePattern,
+  init
+}) {
+  const headersObj = isPrerendered ? void 0 : headers instanceof Headers ? headers : new Headers(
+    // Filter out HTTP/2 pseudo-headers. These are internally-generated headers added to all HTTP/2 requests with trusted metadata about the request.
+    // Examples include `:method`, `:scheme`, `:authority`, and `:path`.
+    // They are always prefixed with a colon to distinguish them from other headers, and it is an error to add the to a Headers object manually.
+    // See https://httpwg.org/specs/rfc7540.html#HttpRequest
+    Object.entries(headers).filter(([name]) => !name.startsWith(":"))
+  );
+  if (typeof url === "string") url = new URL(url);
+  if (isPrerendered) {
+    url.search = "";
+  }
+  const request = new Request(url, {
+    method,
+    headers: headersObj,
+    // body is made available only if the request is for a page that will be on-demand rendered
+    body: isPrerendered ? null : body,
+    ...init
+  });
+  if (isPrerendered) {
+    let _headers = request.headers;
+    const { value, writable, ...headersDesc } = Object.getOwnPropertyDescriptor(request, "headers") || {};
+    Object.defineProperty(request, "headers", {
+      ...headersDesc,
+      get() {
+        logger.warn(
+          null,
+          `\`Astro.request.headers\` was used when rendering the route \`${routePattern}'\`. \`Astro.request.headers\` is not available on prerendered pages. If you need access to request headers, make sure that the page is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your pages server-rendered by default.`
+        );
+        return _headers;
+      },
+      set(newHeaders) {
+        _headers = newHeaders;
+      }
+    });
+  }
+  return request;
+}
+
+function findRouteToRewrite({
+  payload,
+  routes,
+  request,
+  trailingSlash,
+  buildFormat,
+  base,
+  outDir
+}) {
+  let newUrl = void 0;
+  if (payload instanceof URL) {
+    newUrl = payload;
+  } else if (payload instanceof Request) {
+    newUrl = new URL(payload.url);
+  } else {
+    newUrl = new URL(payload, new URL(request.url).origin);
+  }
+  let pathname = newUrl.pathname;
+  const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
+  if (base !== "/") {
+    const isBasePathRequest = newUrl.pathname === base || newUrl.pathname === removeTrailingForwardSlash(base);
+    if (isBasePathRequest) {
+      pathname = shouldAppendSlash ? "/" : "";
+    } else if (newUrl.pathname.startsWith(base)) {
+      pathname = shouldAppendSlash ? appendForwardSlash(newUrl.pathname) : removeTrailingForwardSlash(newUrl.pathname);
+      pathname = pathname.slice(base.length);
+    }
+  }
+  if (!pathname.startsWith("/") && shouldAppendSlash && newUrl.pathname.endsWith("/")) {
+    pathname = prependForwardSlash(pathname);
+  }
+  if (pathname === "/" && base !== "/" && !shouldAppendSlash) {
+    pathname = "";
+  }
+  if (buildFormat === "file") {
+    pathname = pathname.replace(/\.html$/, "");
+  }
+  if (base !== "/" && (pathname === "" || pathname === "/") && !shouldAppendSlash) {
+    newUrl.pathname = removeTrailingForwardSlash(base);
+  } else {
+    newUrl.pathname = joinPaths(...[base, pathname].filter(Boolean));
+  }
+  const decodedPathname = decodeURI(pathname);
+  let foundRoute;
+  for (const route of routes) {
+    if (route.pattern.test(decodedPathname)) {
+      if (route.params && route.params.length !== 0 && route.distURL && route.distURL.length !== 0) {
+        if (!route.distURL.find(
+          (url) => url.href.replace(outDir.toString(), "").replace(/(?:\/index\.html|\.html)$/, "") == trimSlashes(decodedPathname)
+        )) {
+          continue;
+        }
+      }
+      foundRoute = route;
+      break;
+    }
+  }
+  if (foundRoute) {
+    return {
+      routeData: foundRoute,
+      newUrl,
+      pathname: decodedPathname
+    };
+  } else {
+    const custom404 = routes.find((route) => route.route === "/404");
+    if (custom404) {
+      return { routeData: custom404, newUrl, pathname };
+    } else {
+      return { routeData: DEFAULT_404_ROUTE, newUrl, pathname };
+    }
+  }
+}
+function copyRequest(newUrl, oldRequest, isPrerendered, logger, routePattern) {
+  if (oldRequest.bodyUsed) {
+    throw new AstroError(RewriteWithBodyUsed);
+  }
+  return createRequest({
+    url: newUrl,
+    method: oldRequest.method,
+    body: oldRequest.body,
+    isPrerendered,
+    logger,
+    headers: isPrerendered ? {} : oldRequest.headers,
+    routePattern,
+    init: {
+      referrer: oldRequest.referrer,
+      referrerPolicy: oldRequest.referrerPolicy,
+      mode: oldRequest.mode,
+      credentials: oldRequest.credentials,
+      cache: oldRequest.cache,
+      redirect: oldRequest.redirect,
+      integrity: oldRequest.integrity,
+      signal: oldRequest.signal,
+      keepalive: oldRequest.keepalive,
+      // https://fetch.spec.whatwg.org/#dom-request-duplex
+      // @ts-expect-error It isn't part of the types, but undici accepts it and it allows to carry over the body to a new request
+      duplex: "half"
+    }
+  });
+}
+function setOriginPathname(request, pathname, trailingSlash, buildFormat) {
+  if (!pathname) {
+    pathname = "/";
+  }
+  const shouldAppendSlash = shouldAppendForwardSlash(trailingSlash, buildFormat);
+  let finalPathname;
+  if (pathname === "/") {
+    finalPathname = "/";
+  } else if (shouldAppendSlash) {
+    finalPathname = appendForwardSlash(pathname);
+  } else {
+    finalPathname = removeTrailingForwardSlash(pathname);
+  }
+  Reflect.set(request, originPathnameSymbol, encodeURIComponent(finalPathname));
+}
+function getOriginPathname(request) {
+  const origin = Reflect.get(request, originPathnameSymbol);
+  if (origin) {
+    return decodeURIComponent(origin);
+  }
+  return new URL(request.url).pathname;
+}
+
+const VALID_PARAM_TYPES = ["string", "number", "undefined"];
+function validateGetStaticPathsParameter([key, value], route) {
+  if (!VALID_PARAM_TYPES.includes(typeof value)) {
+    throw new AstroError({
+      ...GetStaticPathsInvalidRouteParam,
+      message: GetStaticPathsInvalidRouteParam.message(key, value, typeof value),
+      location: {
+        file: route
+      }
+    });
+  }
+}
+function validateDynamicRouteModule(mod, {
+  ssr,
+  route
+}) {
+  if ((!ssr || route.prerender) && !mod.getStaticPaths) {
+    throw new AstroError({
+      ...GetStaticPathsRequired,
+      location: { file: route.component }
+    });
+  }
+}
+function validateGetStaticPathsResult(result, logger, route) {
+  if (!Array.isArray(result)) {
+    throw new AstroError({
+      ...InvalidGetStaticPathsReturn,
+      message: InvalidGetStaticPathsReturn.message(typeof result),
+      location: {
+        file: route.component
+      }
+    });
+  }
+  result.forEach((pathObject) => {
+    if (typeof pathObject === "object" && Array.isArray(pathObject) || pathObject === null) {
+      throw new AstroError({
+        ...InvalidGetStaticPathsEntry,
+        message: InvalidGetStaticPathsEntry.message(
+          Array.isArray(pathObject) ? "array" : typeof pathObject
+        )
+      });
+    }
+    if (pathObject.params === void 0 || pathObject.params === null || pathObject.params && Object.keys(pathObject.params).length === 0) {
+      throw new AstroError({
+        ...GetStaticPathsExpectedParams,
+        location: {
+          file: route.component
+        }
+      });
+    }
+    for (const [key, val] of Object.entries(pathObject.params)) {
+      if (!(typeof val === "undefined" || typeof val === "string" || typeof val === "number")) {
+        logger.warn(
+          "router",
+          `getStaticPaths() returned an invalid path param: "${key}". A string, number or undefined value was expected, but got \`${JSON.stringify(
+            val
+          )}\`.`
+        );
+      }
+      if (typeof val === "string" && val === "") {
+        logger.warn(
+          "router",
+          `getStaticPaths() returned an invalid path param: "${key}". \`undefined\` expected for an optional param, but got empty string.`
+        );
+      }
+    }
+  });
+}
+
+function stringifyParams(params, route) {
+  const validatedParams = Object.entries(params).reduce((acc, next) => {
+    validateGetStaticPathsParameter(next, route.component);
+    const [key, value] = next;
+    if (value !== void 0) {
+      acc[key] = typeof value === "string" ? trimSlashes(value) : value.toString();
+    }
+    return acc;
+  }, {});
+  return route.generate(validatedParams);
+}
+
+function generatePaginateFunction(routeMatch, base) {
+  return function paginateUtility(data, args = {}) {
+    let { pageSize: _pageSize, params: _params, props: _props } = args;
+    const pageSize = _pageSize || 10;
+    const paramName = "page";
+    const additionalParams = _params || {};
+    const additionalProps = _props || {};
+    let includesFirstPageNumber;
+    if (routeMatch.params.includes(`...${paramName}`)) {
+      includesFirstPageNumber = false;
+    } else if (routeMatch.params.includes(`${paramName}`)) {
+      includesFirstPageNumber = true;
+    } else {
+      throw new AstroError({
+        ...PageNumberParamNotFound,
+        message: PageNumberParamNotFound.message(paramName)
+      });
+    }
+    const lastPage = Math.max(1, Math.ceil(data.length / pageSize));
+    const result = [...Array(lastPage).keys()].map((num) => {
+      const pageNum = num + 1;
+      const start = pageSize === Infinity ? 0 : (pageNum - 1) * pageSize;
+      const end = Math.min(start + pageSize, data.length);
+      const params = {
+        ...additionalParams,
+        [paramName]: includesFirstPageNumber || pageNum > 1 ? String(pageNum) : void 0
+      };
+      const current = addRouteBase(routeMatch.generate({ ...params }), base);
+      const next = pageNum === lastPage ? void 0 : addRouteBase(routeMatch.generate({ ...params, page: String(pageNum + 1) }), base);
+      const prev = pageNum === 1 ? void 0 : addRouteBase(
+        routeMatch.generate({
+          ...params,
+          page: !includesFirstPageNumber && pageNum - 1 === 1 ? void 0 : String(pageNum - 1)
+        }),
+        base
+      );
+      const first = pageNum === 1 ? void 0 : addRouteBase(
+        routeMatch.generate({
+          ...params,
+          page: includesFirstPageNumber ? "1" : void 0
+        }),
+        base
+      );
+      const last = pageNum === lastPage ? void 0 : addRouteBase(routeMatch.generate({ ...params, page: String(lastPage) }), base);
+      return {
+        params,
+        props: {
+          ...additionalProps,
+          page: {
+            data: data.slice(start, end),
+            start,
+            end: end - 1,
+            size: pageSize,
+            total: data.length,
+            currentPage: pageNum,
+            lastPage,
+            url: { current, next, prev, first, last }
+          }
+        }
+      };
+    });
+    return result;
+  };
+}
+function addRouteBase(route, base) {
+  let routeWithBase = joinPaths(base, route);
+  if (routeWithBase === "") routeWithBase = "/";
+  return routeWithBase;
+}
+
+async function callGetStaticPaths({
+  mod,
+  route,
+  routeCache,
+  logger,
+  ssr,
+  base
+}) {
+  const cached = routeCache.get(route);
+  if (!mod) {
+    throw new Error("This is an error caused by Astro and not your code. Please file an issue.");
+  }
+  if (cached?.staticPaths) {
+    return cached.staticPaths;
+  }
+  validateDynamicRouteModule(mod, { ssr, route });
+  if (ssr && !route.prerender) {
+    const entry = Object.assign([], { keyed: /* @__PURE__ */ new Map() });
+    routeCache.set(route, { ...cached, staticPaths: entry });
+    return entry;
+  }
+  let staticPaths = [];
+  if (!mod.getStaticPaths) {
+    throw new Error("Unexpected Error.");
+  }
+  staticPaths = await mod.getStaticPaths({
+    // Q: Why the cast?
+    // A: So users downstream can have nicer typings, we have to make some sacrifice in our internal typings, which necessitate a cast here
+    paginate: generatePaginateFunction(route, base),
+    routePattern: route.route
+  });
+  validateGetStaticPathsResult(staticPaths, logger, route);
+  const keyedStaticPaths = staticPaths;
+  keyedStaticPaths.keyed = /* @__PURE__ */ new Map();
+  for (const sp of keyedStaticPaths) {
+    const paramsKey = stringifyParams(sp.params, route);
+    keyedStaticPaths.keyed.set(paramsKey, sp);
+  }
+  routeCache.set(route, { ...cached, staticPaths: keyedStaticPaths });
+  return keyedStaticPaths;
+}
+class RouteCache {
+  logger;
+  cache = {};
+  runtimeMode;
+  constructor(logger, runtimeMode = "production") {
+    this.logger = logger;
+    this.runtimeMode = runtimeMode;
+  }
+  /** Clear the cache. */
+  clearAll() {
+    this.cache = {};
+  }
+  set(route, entry) {
+    const key = this.key(route);
+    if (this.runtimeMode === "production" && this.cache[key]?.staticPaths) {
+      this.logger.warn(null, `Internal Warning: route cache overwritten. (${key})`);
+    }
+    this.cache[key] = entry;
+  }
+  get(route) {
+    return this.cache[this.key(route)];
+  }
+  key(route) {
+    return `${route.route}_${route.component}`;
+  }
+}
+function findPathItemByKey(staticPaths, params, route, logger) {
+  const paramsKey = stringifyParams(params, route);
+  const matchedStaticPath = staticPaths.keyed.get(paramsKey);
+  if (matchedStaticPath) {
+    return matchedStaticPath;
+  }
+  logger.debug("router", `findPathItemByKey() - Unexpected cache miss looking for ${paramsKey}`);
+}
+
+function routeIsRedirect(route) {
+  return route?.type === "redirect";
+}
+function routeIsFallback(route) {
+  return route?.type === "fallback";
+}
+
+async function getProps(opts) {
+  const { logger, mod, routeData: route, routeCache, pathname, serverLike, base } = opts;
+  if (!route || route.pathname) {
+    return {};
+  }
+  if (routeIsRedirect(route) || routeIsFallback(route) || route.component === DEFAULT_404_COMPONENT) {
+    return {};
+  }
+  const staticPaths = await callGetStaticPaths({
+    mod,
+    route,
+    routeCache,
+    logger,
+    ssr: serverLike,
+    base
+  });
+  const params = getParams(route, pathname);
+  const matchedStaticPath = findPathItemByKey(staticPaths, params, route, logger);
+  if (!matchedStaticPath && (serverLike ? route.prerender : true)) {
+    throw new AstroError({
+      ...NoMatchingStaticPathFound,
+      message: NoMatchingStaticPathFound.message(pathname),
+      hint: NoMatchingStaticPathFound.hint([route.component])
+    });
+  }
+  if (mod) {
+    validatePrerenderEndpointCollision(route, mod, params);
+  }
+  const props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
+  return props;
+}
+function getParams(route, pathname) {
+  if (!route.params.length) return {};
+  const paramsMatch = route.pattern.exec(pathname) || route.fallbackRoutes.map((fallbackRoute) => fallbackRoute.pattern.exec(pathname)).find((x) => x);
+  if (!paramsMatch) return {};
+  const params = {};
+  route.params.forEach((key, i) => {
+    if (key.startsWith("...")) {
+      params[key.slice(3)] = paramsMatch[i + 1] ? paramsMatch[i + 1] : void 0;
+    } else {
+      params[key] = paramsMatch[i + 1];
+    }
+  });
+  return params;
+}
+function validatePrerenderEndpointCollision(route, mod, params) {
+  if (route.type === "endpoint" && mod.getStaticPaths) {
+    const lastSegment = route.segments[route.segments.length - 1];
+    const paramValues = Object.values(params);
+    const lastParam = paramValues[paramValues.length - 1];
+    if (lastSegment.length === 1 && lastSegment[0].dynamic && lastParam === void 0) {
+      throw new AstroError({
+        ...PrerenderDynamicEndpointPathCollide,
+        message: PrerenderDynamicEndpointPathCollide.message(route.route),
+        hint: PrerenderDynamicEndpointPathCollide.hint(route.component),
+        location: {
+          file: route.component
+        }
+      });
+    }
+  }
+}
+
+function getFunctionExpression(slot) {
+  if (!slot) return;
+  const expressions = slot?.expressions?.filter((e) => isRenderInstruction(e) === false);
+  if (expressions?.length !== 1) return;
+  return expressions[0];
+}
+class Slots {
+  #result;
+  #slots;
+  #logger;
+  constructor(result, slots, logger) {
+    this.#result = result;
+    this.#slots = slots;
+    this.#logger = logger;
+    if (slots) {
+      for (const key of Object.keys(slots)) {
+        if (this[key] !== void 0) {
+          throw new AstroError({
+            ...ReservedSlotName,
+            message: ReservedSlotName.message(key)
+          });
+        }
+        Object.defineProperty(this, key, {
+          get() {
+            return true;
+          },
+          enumerable: true
+        });
+      }
+    }
+  }
+  has(name) {
+    if (!this.#slots) return false;
+    return Boolean(this.#slots[name]);
+  }
+  async render(name, args = []) {
+    if (!this.#slots || !this.has(name)) return;
+    const result = this.#result;
+    if (!Array.isArray(args)) {
+      this.#logger.warn(
+        null,
+        `Expected second parameter to be an array, received a ${typeof args}. If you're trying to pass an array as a single argument and getting unexpected results, make sure you're passing your array as a item of an array. Ex: Astro.slots.render('default', [["Hello", "World"]])`
+      );
+    } else if (args.length > 0) {
+      const slotValue = this.#slots[name];
+      const component = typeof slotValue === "function" ? await slotValue(result) : await slotValue;
+      const expression = getFunctionExpression(component);
+      if (expression) {
+        const slot = async () => typeof expression === "function" ? expression(...args) : expression;
+        return await renderSlotToString(result, slot).then((res) => {
+          return res;
+        });
+      }
+      if (typeof component === "function") {
+        return await renderJSX(result, component(...args)).then(
+          (res) => res != null ? String(res) : res
+        );
+      }
+    }
+    const content = await renderSlotToString(result, this.#slots[name]);
+    const outHTML = chunkToString(result, content);
+    return outHTML;
+  }
+}
+
+function getActionContext(context) {
+  const callerInfo = getCallerInfo(context);
+  const actionResultAlreadySet = Boolean(context.locals._actionPayload);
+  let action = void 0;
+  if (callerInfo && context.request.method === "POST" && !actionResultAlreadySet) {
+    action = {
+      calledFrom: callerInfo.from,
+      name: callerInfo.name,
+      handler: async () => {
+        const pipeline = Reflect.get(context, apiContextRoutesSymbol);
+        const callerInfoName = shouldAppendForwardSlash(
+          pipeline.manifest.trailingSlash,
+          pipeline.manifest.buildFormat
+        ) ? removeTrailingForwardSlash(callerInfo.name) : callerInfo.name;
+        const baseAction = await pipeline.getAction(callerInfoName);
+        let input;
+        try {
+          input = await parseRequestBody(context.request);
+        } catch (e) {
+          if (e instanceof TypeError) {
+            return { data: void 0, error: new ActionError({ code: "UNSUPPORTED_MEDIA_TYPE" }) };
+          }
+          throw e;
+        }
+        const omitKeys = ["props", "getActionResult", "callAction", "redirect"];
+        const actionAPIContext = Object.create(
+          Object.getPrototypeOf(context),
+          Object.fromEntries(
+            Object.entries(Object.getOwnPropertyDescriptors(context)).filter(
+              ([key]) => !omitKeys.includes(key)
+            )
+          )
+        );
+        Reflect.set(actionAPIContext, ACTION_API_CONTEXT_SYMBOL, true);
+        const handler = baseAction.bind(actionAPIContext);
+        return handler(input);
+      }
+    };
+  }
+  function setActionResult(actionName, actionResult) {
+    context.locals._actionPayload = {
+      actionResult,
+      actionName
+    };
+  }
+  return {
+    action,
+    setActionResult,
+    serializeActionResult,
+    deserializeActionResult
+  };
+}
+function getCallerInfo(ctx) {
+  if (ctx.routePattern === ACTION_RPC_ROUTE_PATTERN) {
+    return { from: "rpc", name: ctx.url.pathname.replace(/^.*\/_actions\//, "") };
+  }
+  const queryParam = ctx.url.searchParams.get(ACTION_QUERY_PARAMS.actionName);
+  if (queryParam) {
+    return { from: "form", name: queryParam };
+  }
+  return void 0;
+}
+async function parseRequestBody(request) {
+  const contentType = request.headers.get("content-type");
+  const contentLength = request.headers.get("Content-Length");
+  if (!contentType) return void 0;
+  if (hasContentType(contentType, formContentTypes)) {
+    return await request.clone().formData();
+  }
+  if (hasContentType(contentType, ["application/json"])) {
+    return contentLength === "0" ? void 0 : await request.clone().json();
+  }
+  throw new TypeError("Unsupported content type");
+}
+
+async function callMiddleware(onRequest, apiContext, responseFunction) {
+  let nextCalled = false;
+  let responseFunctionPromise = void 0;
+  const next = async (payload) => {
+    nextCalled = true;
+    responseFunctionPromise = responseFunction(apiContext, payload);
+    return responseFunctionPromise;
+  };
+  let middlewarePromise = onRequest(apiContext, next);
+  return await Promise.resolve(middlewarePromise).then(async (value) => {
+    if (nextCalled) {
+      if (typeof value !== "undefined") {
+        if (value instanceof Response === false) {
+          throw new AstroError(MiddlewareNotAResponse);
+        }
+        return value;
+      } else {
+        if (responseFunctionPromise) {
+          return responseFunctionPromise;
+        } else {
+          throw new AstroError(MiddlewareNotAResponse);
+        }
+      }
+    } else if (typeof value === "undefined") {
+      throw new AstroError(MiddlewareNoDataOrNextCalled);
+    } else if (value instanceof Response === false) {
+      throw new AstroError(MiddlewareNotAResponse);
+    } else {
+      return value;
+    }
+  });
+}
+
+const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/;
+const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
+const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;
+function jsonParseTransform(key, value) {
+  if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) {
+    warnKeyDropped(key);
+    return;
+  }
+  return value;
+}
+function warnKeyDropped(key) {
+  console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`);
+}
+function destr(value, options = {}) {
+  if (typeof value !== "string") {
+    return value;
+  }
+  if (value[0] === '"' && value[value.length - 1] === '"' && value.indexOf("\\") === -1) {
+    return value.slice(1, -1);
+  }
+  const _value = value.trim();
+  if (_value.length <= 9) {
+    switch (_value.toLowerCase()) {
+      case "true": {
+        return true;
+      }
+      case "false": {
+        return false;
+      }
+      case "undefined": {
+        return void 0;
+      }
+      case "null": {
+        return null;
+      }
+      case "nan": {
+        return Number.NaN;
+      }
+      case "infinity": {
+        return Number.POSITIVE_INFINITY;
+      }
+      case "-infinity": {
+        return Number.NEGATIVE_INFINITY;
+      }
+    }
+  }
+  if (!JsonSigRx.test(value)) {
+    if (options.strict) {
+      throw new SyntaxError("[destr] Invalid JSON");
+    }
+    return value;
+  }
+  try {
+    if (suspectProtoRx.test(value) || suspectConstructorRx.test(value)) {
+      if (options.strict) {
+        throw new Error("[destr] Possible prototype pollution");
+      }
+      return JSON.parse(value, jsonParseTransform);
+    }
+    return JSON.parse(value);
+  } catch (error) {
+    if (options.strict) {
+      throw error;
+    }
+    return value;
+  }
+}
+
+function wrapToPromise(value) {
+  if (!value || typeof value.then !== "function") {
+    return Promise.resolve(value);
+  }
+  return value;
+}
+function asyncCall(function_, ...arguments_) {
+  try {
+    return wrapToPromise(function_(...arguments_));
+  } catch (error) {
+    return Promise.reject(error);
+  }
+}
+function isPrimitive(value) {
+  const type = typeof value;
+  return value === null || type !== "object" && type !== "function";
+}
+function isPureObject(value) {
+  const proto = Object.getPrototypeOf(value);
+  return !proto || proto.isPrototypeOf(Object);
+}
+function stringify$1(value) {
+  if (isPrimitive(value)) {
+    return String(value);
+  }
+  if (isPureObject(value) || Array.isArray(value)) {
+    return JSON.stringify(value);
+  }
+  if (typeof value.toJSON === "function") {
+    return stringify$1(value.toJSON());
+  }
+  throw new Error("[unstorage] Cannot stringify value!");
+}
+const BASE64_PREFIX = "base64:";
+function serializeRaw(value) {
+  if (typeof value === "string") {
+    return value;
+  }
+  return BASE64_PREFIX + base64Encode(value);
+}
+function deserializeRaw(value) {
+  if (typeof value !== "string") {
+    return value;
+  }
+  if (!value.startsWith(BASE64_PREFIX)) {
+    return value;
+  }
+  return base64Decode(value.slice(BASE64_PREFIX.length));
+}
+function base64Decode(input) {
+  if (globalThis.Buffer) {
+    return Buffer.from(input, "base64");
+  }
+  return Uint8Array.from(
+    globalThis.atob(input),
+    (c) => c.codePointAt(0)
+  );
+}
+function base64Encode(input) {
+  if (globalThis.Buffer) {
+    return Buffer.from(input).toString("base64");
+  }
+  return globalThis.btoa(String.fromCodePoint(...input));
+}
+function normalizeKey(key) {
+  if (!key) {
+    return "";
+  }
+  return key.split("?")[0]?.replace(/[/\\]/g, ":").replace(/:+/g, ":").replace(/^:|:$/g, "") || "";
+}
+function joinKeys(...keys) {
+  return normalizeKey(keys.join(":"));
+}
+function normalizeBaseKey(base) {
+  base = normalizeKey(base);
+  return base ? base + ":" : "";
+}
+function filterKeyByDepth(key, depth) {
+  if (depth === void 0) {
+    return true;
+  }
+  let substrCount = 0;
+  let index = key.indexOf(":");
+  while (index > -1) {
+    substrCount++;
+    index = key.indexOf(":", index + 1);
+  }
+  return substrCount <= depth;
+}
+function filterKeyByBase(key, base) {
+  if (base) {
+    return key.startsWith(base) && key[key.length - 1] !== "$";
+  }
+  return key[key.length - 1] !== "$";
+}
+
+function defineDriver(factory) {
+  return factory;
+}
+
+const DRIVER_NAME = "memory";
+const memory = defineDriver(() => {
+  const data = /* @__PURE__ */ new Map();
+  return {
+    name: DRIVER_NAME,
+    getInstance: () => data,
+    hasItem(key) {
+      return data.has(key);
+    },
+    getItem(key) {
+      return data.get(key) ?? null;
+    },
+    getItemRaw(key) {
+      return data.get(key) ?? null;
+    },
+    setItem(key, value) {
+      data.set(key, value);
+    },
+    setItemRaw(key, value) {
+      data.set(key, value);
+    },
+    removeItem(key) {
+      data.delete(key);
+    },
+    getKeys() {
+      return [...data.keys()];
+    },
+    clear() {
+      data.clear();
+    },
+    dispose() {
+      data.clear();
+    }
+  };
+});
+
+function createStorage(options = {}) {
+  const context = {
+    mounts: { "": options.driver || memory() },
+    mountpoints: [""],
+    watching: false,
+    watchListeners: [],
+    unwatch: {}
+  };
+  const getMount = (key) => {
+    for (const base of context.mountpoints) {
+      if (key.startsWith(base)) {
+        return {
+          base,
+          relativeKey: key.slice(base.length),
+          driver: context.mounts[base]
+        };
+      }
+    }
+    return {
+      base: "",
+      relativeKey: key,
+      driver: context.mounts[""]
+    };
+  };
+  const getMounts = (base, includeParent) => {
+    return context.mountpoints.filter(
+      (mountpoint) => mountpoint.startsWith(base) || includeParent && base.startsWith(mountpoint)
+    ).map((mountpoint) => ({
+      relativeBase: base.length > mountpoint.length ? base.slice(mountpoint.length) : void 0,
+      mountpoint,
+      driver: context.mounts[mountpoint]
+    }));
+  };
+  const onChange = (event, key) => {
+    if (!context.watching) {
+      return;
+    }
+    key = normalizeKey(key);
+    for (const listener of context.watchListeners) {
+      listener(event, key);
+    }
+  };
+  const startWatch = async () => {
+    if (context.watching) {
+      return;
+    }
+    context.watching = true;
+    for (const mountpoint in context.mounts) {
+      context.unwatch[mountpoint] = await watch(
+        context.mounts[mountpoint],
+        onChange,
+        mountpoint
+      );
+    }
+  };
+  const stopWatch = async () => {
+    if (!context.watching) {
+      return;
+    }
+    for (const mountpoint in context.unwatch) {
+      await context.unwatch[mountpoint]();
+    }
+    context.unwatch = {};
+    context.watching = false;
+  };
+  const runBatch = (items, commonOptions, cb) => {
+    const batches = /* @__PURE__ */ new Map();
+    const getBatch = (mount) => {
+      let batch = batches.get(mount.base);
+      if (!batch) {
+        batch = {
+          driver: mount.driver,
+          base: mount.base,
+          items: []
+        };
+        batches.set(mount.base, batch);
+      }
+      return batch;
+    };
+    for (const item of items) {
+      const isStringItem = typeof item === "string";
+      const key = normalizeKey(isStringItem ? item : item.key);
+      const value = isStringItem ? void 0 : item.value;
+      const options2 = isStringItem || !item.options ? commonOptions : { ...commonOptions, ...item.options };
+      const mount = getMount(key);
+      getBatch(mount).items.push({
+        key,
+        value,
+        relativeKey: mount.relativeKey,
+        options: options2
+      });
+    }
+    return Promise.all([...batches.values()].map((batch) => cb(batch))).then(
+      (r) => r.flat()
+    );
+  };
+  const storage = {
+    // Item
+    hasItem(key, opts = {}) {
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      return asyncCall(driver.hasItem, relativeKey, opts);
+    },
+    getItem(key, opts = {}) {
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      return asyncCall(driver.getItem, relativeKey, opts).then(
+        (value) => destr(value)
+      );
+    },
+    getItems(items, commonOptions = {}) {
+      return runBatch(items, commonOptions, (batch) => {
+        if (batch.driver.getItems) {
+          return asyncCall(
+            batch.driver.getItems,
+            batch.items.map((item) => ({
+              key: item.relativeKey,
+              options: item.options
+            })),
+            commonOptions
+          ).then(
+            (r) => r.map((item) => ({
+              key: joinKeys(batch.base, item.key),
+              value: destr(item.value)
+            }))
+          );
+        }
+        return Promise.all(
+          batch.items.map((item) => {
+            return asyncCall(
+              batch.driver.getItem,
+              item.relativeKey,
+              item.options
+            ).then((value) => ({
+              key: item.key,
+              value: destr(value)
+            }));
+          })
+        );
+      });
+    },
+    getItemRaw(key, opts = {}) {
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      if (driver.getItemRaw) {
+        return asyncCall(driver.getItemRaw, relativeKey, opts);
+      }
+      return asyncCall(driver.getItem, relativeKey, opts).then(
+        (value) => deserializeRaw(value)
+      );
+    },
+    async setItem(key, value, opts = {}) {
+      if (value === void 0) {
+        return storage.removeItem(key);
+      }
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      if (!driver.setItem) {
+        return;
+      }
+      await asyncCall(driver.setItem, relativeKey, stringify$1(value), opts);
+      if (!driver.watch) {
+        onChange("update", key);
+      }
+    },
+    async setItems(items, commonOptions) {
+      await runBatch(items, commonOptions, async (batch) => {
+        if (batch.driver.setItems) {
+          return asyncCall(
+            batch.driver.setItems,
+            batch.items.map((item) => ({
+              key: item.relativeKey,
+              value: stringify$1(item.value),
+              options: item.options
+            })),
+            commonOptions
+          );
+        }
+        if (!batch.driver.setItem) {
+          return;
+        }
+        await Promise.all(
+          batch.items.map((item) => {
+            return asyncCall(
+              batch.driver.setItem,
+              item.relativeKey,
+              stringify$1(item.value),
+              item.options
+            );
+          })
+        );
+      });
+    },
+    async setItemRaw(key, value, opts = {}) {
+      if (value === void 0) {
+        return storage.removeItem(key, opts);
+      }
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      if (driver.setItemRaw) {
+        await asyncCall(driver.setItemRaw, relativeKey, value, opts);
+      } else if (driver.setItem) {
+        await asyncCall(driver.setItem, relativeKey, serializeRaw(value), opts);
+      } else {
+        return;
+      }
+      if (!driver.watch) {
+        onChange("update", key);
+      }
+    },
+    async removeItem(key, opts = {}) {
+      if (typeof opts === "boolean") {
+        opts = { removeMeta: opts };
+      }
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      if (!driver.removeItem) {
+        return;
+      }
+      await asyncCall(driver.removeItem, relativeKey, opts);
+      if (opts.removeMeta || opts.removeMata) {
+        await asyncCall(driver.removeItem, relativeKey + "$", opts);
+      }
+      if (!driver.watch) {
+        onChange("remove", key);
+      }
+    },
+    // Meta
+    async getMeta(key, opts = {}) {
+      if (typeof opts === "boolean") {
+        opts = { nativeOnly: opts };
+      }
+      key = normalizeKey(key);
+      const { relativeKey, driver } = getMount(key);
+      const meta = /* @__PURE__ */ Object.create(null);
+      if (driver.getMeta) {
+        Object.assign(meta, await asyncCall(driver.getMeta, relativeKey, opts));
+      }
+      if (!opts.nativeOnly) {
+        const value = await asyncCall(
+          driver.getItem,
+          relativeKey + "$",
+          opts
+        ).then((value_) => destr(value_));
+        if (value && typeof value === "object") {
+          if (typeof value.atime === "string") {
+            value.atime = new Date(value.atime);
+          }
+          if (typeof value.mtime === "string") {
+            value.mtime = new Date(value.mtime);
+          }
+          Object.assign(meta, value);
+        }
+      }
+      return meta;
+    },
+    setMeta(key, value, opts = {}) {
+      return this.setItem(key + "$", value, opts);
+    },
+    removeMeta(key, opts = {}) {
+      return this.removeItem(key + "$", opts);
+    },
+    // Keys
+    async getKeys(base, opts = {}) {
+      base = normalizeBaseKey(base);
+      const mounts = getMounts(base, true);
+      let maskedMounts = [];
+      const allKeys = [];
+      let allMountsSupportMaxDepth = true;
+      for (const mount of mounts) {
+        if (!mount.driver.flags?.maxDepth) {
+          allMountsSupportMaxDepth = false;
+        }
+        const rawKeys = await asyncCall(
+          mount.driver.getKeys,
+          mount.relativeBase,
+          opts
+        );
+        for (const key of rawKeys) {
+          const fullKey = mount.mountpoint + normalizeKey(key);
+          if (!maskedMounts.some((p) => fullKey.startsWith(p))) {
+            allKeys.push(fullKey);
+          }
+        }
+        maskedMounts = [
+          mount.mountpoint,
+          ...maskedMounts.filter((p) => !p.startsWith(mount.mountpoint))
+        ];
+      }
+      const shouldFilterByDepth = opts.maxDepth !== void 0 && !allMountsSupportMaxDepth;
+      return allKeys.filter(
+        (key) => (!shouldFilterByDepth || filterKeyByDepth(key, opts.maxDepth)) && filterKeyByBase(key, base)
+      );
+    },
+    // Utils
+    async clear(base, opts = {}) {
+      base = normalizeBaseKey(base);
+      await Promise.all(
+        getMounts(base, false).map(async (m) => {
+          if (m.driver.clear) {
+            return asyncCall(m.driver.clear, m.relativeBase, opts);
+          }
+          if (m.driver.removeItem) {
+            const keys = await m.driver.getKeys(m.relativeBase || "", opts);
+            return Promise.all(
+              keys.map((key) => m.driver.removeItem(key, opts))
+            );
+          }
+        })
+      );
+    },
+    async dispose() {
+      await Promise.all(
+        Object.values(context.mounts).map((driver) => dispose(driver))
+      );
+    },
+    async watch(callback) {
+      await startWatch();
+      context.watchListeners.push(callback);
+      return async () => {
+        context.watchListeners = context.watchListeners.filter(
+          (listener) => listener !== callback
+        );
+        if (context.watchListeners.length === 0) {
+          await stopWatch();
+        }
+      };
+    },
+    async unwatch() {
+      context.watchListeners = [];
+      await stopWatch();
+    },
+    // Mount
+    mount(base, driver) {
+      base = normalizeBaseKey(base);
+      if (base && context.mounts[base]) {
+        throw new Error(`already mounted at ${base}`);
+      }
+      if (base) {
+        context.mountpoints.push(base);
+        context.mountpoints.sort((a, b) => b.length - a.length);
+      }
+      context.mounts[base] = driver;
+      if (context.watching) {
+        Promise.resolve(watch(driver, onChange, base)).then((unwatcher) => {
+          context.unwatch[base] = unwatcher;
+        }).catch(console.error);
+      }
+      return storage;
+    },
+    async unmount(base, _dispose = true) {
+      base = normalizeBaseKey(base);
+      if (!base || !context.mounts[base]) {
+        return;
+      }
+      if (context.watching && base in context.unwatch) {
+        context.unwatch[base]?.();
+        delete context.unwatch[base];
+      }
+      if (_dispose) {
+        await dispose(context.mounts[base]);
+      }
+      context.mountpoints = context.mountpoints.filter((key) => key !== base);
+      delete context.mounts[base];
+    },
+    getMount(key = "") {
+      key = normalizeKey(key) + ":";
+      const m = getMount(key);
+      return {
+        driver: m.driver,
+        base: m.base
+      };
+    },
+    getMounts(base = "", opts = {}) {
+      base = normalizeKey(base);
+      const mounts = getMounts(base, opts.parents);
+      return mounts.map((m) => ({
+        driver: m.driver,
+        base: m.mountpoint
+      }));
+    },
+    // Aliases
+    keys: (base, opts = {}) => storage.getKeys(base, opts),
+    get: (key, opts = {}) => storage.getItem(key, opts),
+    set: (key, value, opts = {}) => storage.setItem(key, value, opts),
+    has: (key, opts = {}) => storage.hasItem(key, opts),
+    del: (key, opts = {}) => storage.removeItem(key, opts),
+    remove: (key, opts = {}) => storage.removeItem(key, opts)
+  };
+  return storage;
+}
+function watch(driver, onChange, base) {
+  return driver.watch ? driver.watch((event, key) => onChange(event, base + key)) : () => {
+  };
+}
+async function dispose(driver) {
+  if (typeof driver.dispose === "function") {
+    await asyncCall(driver.dispose);
+  }
+}
+
+const builtinDrivers = {
+  "azure-app-configuration": "unstorage/drivers/azure-app-configuration",
+  "azureAppConfiguration": "unstorage/drivers/azure-app-configuration",
+  "azure-cosmos": "unstorage/drivers/azure-cosmos",
+  "azureCosmos": "unstorage/drivers/azure-cosmos",
+  "azure-key-vault": "unstorage/drivers/azure-key-vault",
+  "azureKeyVault": "unstorage/drivers/azure-key-vault",
+  "azure-storage-blob": "unstorage/drivers/azure-storage-blob",
+  "azureStorageBlob": "unstorage/drivers/azure-storage-blob",
+  "azure-storage-table": "unstorage/drivers/azure-storage-table",
+  "azureStorageTable": "unstorage/drivers/azure-storage-table",
+  "capacitor-preferences": "unstorage/drivers/capacitor-preferences",
+  "capacitorPreferences": "unstorage/drivers/capacitor-preferences",
+  "cloudflare-kv-binding": "unstorage/drivers/cloudflare-kv-binding",
+  "cloudflareKVBinding": "unstorage/drivers/cloudflare-kv-binding",
+  "cloudflare-kv-http": "unstorage/drivers/cloudflare-kv-http",
+  "cloudflareKVHttp": "unstorage/drivers/cloudflare-kv-http",
+  "cloudflare-r2-binding": "unstorage/drivers/cloudflare-r2-binding",
+  "cloudflareR2Binding": "unstorage/drivers/cloudflare-r2-binding",
+  "db0": "unstorage/drivers/db0",
+  "deno-kv-node": "unstorage/drivers/deno-kv-node",
+  "denoKVNode": "unstorage/drivers/deno-kv-node",
+  "deno-kv": "unstorage/drivers/deno-kv",
+  "denoKV": "unstorage/drivers/deno-kv",
+  "fs-lite": "unstorage/drivers/fs-lite",
+  "fsLite": "unstorage/drivers/fs-lite",
+  "fs": "unstorage/drivers/fs",
+  "github": "unstorage/drivers/github",
+  "http": "unstorage/drivers/http",
+  "indexedb": "unstorage/drivers/indexedb",
+  "localstorage": "unstorage/drivers/localstorage",
+  "lru-cache": "unstorage/drivers/lru-cache",
+  "lruCache": "unstorage/drivers/lru-cache",
+  "memory": "unstorage/drivers/memory",
+  "mongodb": "unstorage/drivers/mongodb",
+  "netlify-blobs": "unstorage/drivers/netlify-blobs",
+  "netlifyBlobs": "unstorage/drivers/netlify-blobs",
+  "null": "unstorage/drivers/null",
+  "overlay": "unstorage/drivers/overlay",
+  "planetscale": "unstorage/drivers/planetscale",
+  "redis": "unstorage/drivers/redis",
+  "s3": "unstorage/drivers/s3",
+  "session-storage": "unstorage/drivers/session-storage",
+  "sessionStorage": "unstorage/drivers/session-storage",
+  "uploadthing": "unstorage/drivers/uploadthing",
+  "upstash": "unstorage/drivers/upstash",
+  "vercel-blob": "unstorage/drivers/vercel-blob",
+  "vercelBlob": "unstorage/drivers/vercel-blob",
+  "vercel-kv": "unstorage/drivers/vercel-kv",
+  "vercelKV": "unstorage/drivers/vercel-kv",
+  "vercel-runtime-cache": "unstorage/drivers/vercel-runtime-cache",
+  "vercelRuntimeCache": "unstorage/drivers/vercel-runtime-cache"
+};
+
+const PERSIST_SYMBOL = Symbol();
+const DEFAULT_COOKIE_NAME = "astro-session";
+const VALID_COOKIE_REGEX = /^[\w-]+$/;
+const unflatten = (parsed, _) => {
+  return unflatten$1(parsed, {
+    URL: (href) => new URL(href)
+  });
+};
+const stringify = (data, _) => {
+  return stringify$2(data, {
+    // Support URL objects
+    URL: (val) => val instanceof URL && val.href
+  });
+};
+class AstroSession {
+  // The cookies object.
+  #cookies;
+  // The session configuration.
+  #config;
+  // The cookie config
+  #cookieConfig;
+  // The cookie name
+  #cookieName;
+  // The unstorage object for the session driver.
+  #storage;
+  #data;
+  // The session ID. A v4 UUID.
+  #sessionID;
+  // Sessions to destroy. Needed because we won't have the old session ID after it's destroyed locally.
+  #toDestroy = /* @__PURE__ */ new Set();
+  // Session keys to delete. Used for partial data sets to avoid overwriting the deleted value.
+  #toDelete = /* @__PURE__ */ new Set();
+  // Whether the session is dirty and needs to be saved.
+  #dirty = false;
+  // Whether the session cookie has been set.
+  #cookieSet = false;
+  // The local data is "partial" if it has not been loaded from storage yet and only
+  // contains values that have been set or deleted in-memory locally.
+  // We do this to avoid the need to block on loading data when it is only being set.
+  // When we load the data from storage, we need to merge it with the local partial data,
+  // preserving in-memory changes and deletions.
+  #partial = true;
+  static #sharedStorage = /* @__PURE__ */ new Map();
+  constructor(cookies, {
+    cookie: cookieConfig = DEFAULT_COOKIE_NAME,
+    ...config
+  }, runtimeMode) {
+    const { driver } = config;
+    if (!driver) {
+      throw new AstroError({
+        ...SessionStorageInitError,
+        message: SessionStorageInitError.message(
+          "No driver was defined in the session configuration and the adapter did not provide a default driver."
+        )
+      });
+    }
+    this.#cookies = cookies;
+    let cookieConfigObject;
+    if (typeof cookieConfig === "object") {
+      const { name = DEFAULT_COOKIE_NAME, ...rest } = cookieConfig;
+      this.#cookieName = name;
+      cookieConfigObject = rest;
+    } else {
+      this.#cookieName = cookieConfig || DEFAULT_COOKIE_NAME;
+    }
+    this.#cookieConfig = {
+      sameSite: "lax",
+      secure: runtimeMode === "production",
+      path: "/",
+      ...cookieConfigObject,
+      httpOnly: true
+    };
+    this.#config = { ...config, driver };
+  }
+  /**
+   * Gets a session value. Returns `undefined` if the session or value does not exist.
+   */
+  async get(key) {
+    return (await this.#ensureData()).get(key)?.data;
+  }
+  /**
+   * Checks if a session value exists.
+   */
+  async has(key) {
+    return (await this.#ensureData()).has(key);
+  }
+  /**
+   * Gets all session values.
+   */
+  async keys() {
+    return (await this.#ensureData()).keys();
+  }
+  /**
+   * Gets all session values.
+   */
+  async values() {
+    return [...(await this.#ensureData()).values()].map((entry) => entry.data);
+  }
+  /**
+   * Gets all session entries.
+   */
+  async entries() {
+    return [...(await this.#ensureData()).entries()].map(([key, entry]) => [key, entry.data]);
+  }
+  /**
+   * Deletes a session value.
+   */
+  delete(key) {
+    this.#data?.delete(key);
+    if (this.#partial) {
+      this.#toDelete.add(key);
+    }
+    this.#dirty = true;
+  }
+  /**
+   * Sets a session value. The session is created if it does not exist.
+   */
+  set(key, value, { ttl } = {}) {
+    if (!key) {
+      throw new AstroError({
+        ...SessionStorageSaveError,
+        message: "The session key was not provided."
+      });
+    }
+    let cloned;
+    try {
+      cloned = unflatten(JSON.parse(stringify(value)));
+    } catch (err) {
+      throw new AstroError(
+        {
+          ...SessionStorageSaveError,
+          message: `The session data for ${key} could not be serialized.`,
+          hint: "See the devalue library for all supported types: https://github.com/rich-harris/devalue"
+        },
+        { cause: err }
+      );
+    }
+    if (!this.#cookieSet) {
+      this.#setCookie();
+      this.#cookieSet = true;
+    }
+    this.#data ??= /* @__PURE__ */ new Map();
+    const lifetime = ttl ?? this.#config.ttl;
+    const expires = typeof lifetime === "number" ? Date.now() + lifetime * 1e3 : lifetime;
+    this.#data.set(key, {
+      data: cloned,
+      expires
+    });
+    this.#dirty = true;
+  }
+  /**
+   * Destroys the session, clearing the cookie and storage if it exists.
+   */
+  destroy() {
+    const sessionId = this.#sessionID ?? this.#cookies.get(this.#cookieName)?.value;
+    if (sessionId) {
+      this.#toDestroy.add(sessionId);
+    }
+    this.#cookies.delete(this.#cookieName, this.#cookieConfig);
+    this.#sessionID = void 0;
+    this.#data = void 0;
+    this.#dirty = true;
+  }
+  /**
+   * Regenerates the session, creating a new session ID. The existing session data is preserved.
+   */
+  async regenerate() {
+    let data = /* @__PURE__ */ new Map();
+    try {
+      data = await this.#ensureData();
+    } catch (err) {
+      console.error("Failed to load session data during regeneration:", err);
+    }
+    const oldSessionId = this.#sessionID;
+    this.#sessionID = crypto.randomUUID();
+    this.#data = data;
+    await this.#setCookie();
+    if (oldSessionId && this.#storage) {
+      this.#storage.removeItem(oldSessionId).catch((err) => {
+        console.error("Failed to remove old session data:", err);
+      });
+    }
+  }
+  // Persists the session data to storage.
+  // This is called automatically at the end of the request.
+  // Uses a symbol to prevent users from calling it directly.
+  async [PERSIST_SYMBOL]() {
+    if (!this.#dirty && !this.#toDestroy.size) {
+      return;
+    }
+    const storage = await this.#ensureStorage();
+    if (this.#dirty && this.#data) {
+      const data = await this.#ensureData();
+      this.#toDelete.forEach((key2) => data.delete(key2));
+      const key = this.#ensureSessionID();
+      let serialized;
+      try {
+        serialized = stringify(data);
+      } catch (err) {
+        throw new AstroError(
+          {
+            ...SessionStorageSaveError,
+            message: SessionStorageSaveError.message(
+              "The session data could not be serialized.",
+              this.#config.driver
+            )
+          },
+          { cause: err }
+        );
+      }
+      await storage.setItem(key, serialized);
+      this.#dirty = false;
+    }
+    if (this.#toDestroy.size > 0) {
+      const cleanupPromises = [...this.#toDestroy].map(
+        (sessionId) => storage.removeItem(sessionId).catch((err) => {
+          console.error(`Failed to clean up session ${sessionId}:`, err);
+        })
+      );
+      await Promise.all(cleanupPromises);
+      this.#toDestroy.clear();
+    }
+  }
+  get sessionID() {
+    return this.#sessionID;
+  }
+  /**
+   * Loads a session from storage with the given ID, and replaces the current session.
+   * Any changes made to the current session will be lost.
+   * This is not normally needed, as the session is automatically loaded using the cookie.
+   * However it can be used to restore a session where the ID has been recorded somewhere
+   * else (e.g. in a database).
+   */
+  async load(sessionID) {
+    this.#sessionID = sessionID;
+    this.#data = void 0;
+    await this.#setCookie();
+    await this.#ensureData();
+  }
+  /**
+   * Sets the session cookie.
+   */
+  async #setCookie() {
+    if (!VALID_COOKIE_REGEX.test(this.#cookieName)) {
+      throw new AstroError({
+        ...SessionStorageSaveError,
+        message: "Invalid cookie name. Cookie names can only contain letters, numbers, and dashes."
+      });
+    }
+    const value = this.#ensureSessionID();
+    this.#cookies.set(this.#cookieName, value, this.#cookieConfig);
+  }
+  /**
+   * Attempts to load the session data from storage, or creates a new data object if none exists.
+   * If there is existing partial data, it will be merged into the new data object.
+   */
+  async #ensureData() {
+    const storage = await this.#ensureStorage();
+    if (this.#data && !this.#partial) {
+      return this.#data;
+    }
+    this.#data ??= /* @__PURE__ */ new Map();
+    const raw = await storage.get(this.#ensureSessionID());
+    if (!raw) {
+      return this.#data;
+    }
+    try {
+      const storedMap = unflatten(raw);
+      if (!(storedMap instanceof Map)) {
+        await this.destroy();
+        throw new AstroError({
+          ...SessionStorageInitError,
+          message: SessionStorageInitError.message(
+            "The session data was an invalid type.",
+            this.#config.driver
+          )
+        });
+      }
+      const now = Date.now();
+      for (const [key, value] of storedMap) {
+        const expired = typeof value.expires === "number" && value.expires < now;
+        if (!this.#data.has(key) && !this.#toDelete.has(key) && !expired) {
+          this.#data.set(key, value);
+        }
+      }
+      this.#partial = false;
+      return this.#data;
+    } catch (err) {
+      await this.destroy();
+      if (err instanceof AstroError) {
+        throw err;
+      }
+      throw new AstroError(
+        {
+          ...SessionStorageInitError,
+          message: SessionStorageInitError.message(
+            "The session data could not be parsed.",
+            this.#config.driver
+          )
+        },
+        { cause: err }
+      );
+    }
+  }
+  /**
+   * Returns the session ID, generating a new one if it does not exist.
+   */
+  #ensureSessionID() {
+    this.#sessionID ??= this.#cookies.get(this.#cookieName)?.value ?? crypto.randomUUID();
+    return this.#sessionID;
+  }
+  /**
+   * Ensures the storage is initialized.
+   * This is called automatically when a storage operation is needed.
+   */
+  async #ensureStorage() {
+    if (this.#storage) {
+      return this.#storage;
+    }
+    if (AstroSession.#sharedStorage.has(this.#config.driver)) {
+      this.#storage = AstroSession.#sharedStorage.get(this.#config.driver);
+      return this.#storage;
+    }
+    if (this.#config.driver === "test") {
+      this.#storage = this.#config.options.mockStorage;
+      return this.#storage;
+    }
+    if (this.#config.driver === "fs" || this.#config.driver === "fsLite" || this.#config.driver === "fs-lite") {
+      this.#config.options ??= {};
+      this.#config.driver = "fs-lite";
+      this.#config.options.base ??= ".astro/session";
+    }
+    let driver = null;
+    try {
+      if (this.#config.driverModule) {
+        driver = (await this.#config.driverModule()).default;
+      } else if (this.#config.driver) {
+        const driverName = resolveSessionDriverName(this.#config.driver);
+        if (driverName) {
+          driver = (await import(driverName)).default;
+        }
+      }
+    } catch (err) {
+      if (err.code === "ERR_MODULE_NOT_FOUND") {
+        throw new AstroError(
+          {
+            ...SessionStorageInitError,
+            message: SessionStorageInitError.message(
+              err.message.includes(`Cannot find package`) ? "The driver module could not be found." : err.message,
+              this.#config.driver
+            )
+          },
+          { cause: err }
+        );
+      }
+      throw err;
+    }
+    if (!driver) {
+      throw new AstroError({
+        ...SessionStorageInitError,
+        message: SessionStorageInitError.message(
+          "The module did not export a driver.",
+          this.#config.driver
+        )
+      });
+    }
+    try {
+      this.#storage = createStorage({
+        driver: driver(this.#config.options)
+      });
+      AstroSession.#sharedStorage.set(this.#config.driver, this.#storage);
+      return this.#storage;
+    } catch (err) {
+      throw new AstroError(
+        {
+          ...SessionStorageInitError,
+          message: SessionStorageInitError.message("Unknown error", this.#config.driver)
+        },
+        { cause: err }
+      );
+    }
+  }
+}
+function resolveSessionDriverName(driver) {
+  if (!driver) {
+    return null;
+  }
+  try {
+    if (driver === "fs") {
+      return builtinDrivers.fsLite;
+    }
+    if (driver in builtinDrivers) {
+      return builtinDrivers[driver];
+    }
+  } catch {
+    return null;
+  }
+  return driver;
+}
+
+const apiContextRoutesSymbol = Symbol.for("context.routes");
+class RenderContext {
+  constructor(pipeline, locals, middleware, actions, pathname, request, routeData, status, clientAddress, cookies = new AstroCookies(request), params = getParams(routeData, pathname), url = new URL(request.url), props = {}, partial = void 0, shouldInjectCspMetaTags = !!pipeline.manifest.csp, session = pipeline.manifest.sessionConfig ? new AstroSession(cookies, pipeline.manifest.sessionConfig, pipeline.runtimeMode) : void 0) {
+    this.pipeline = pipeline;
+    this.locals = locals;
+    this.middleware = middleware;
+    this.actions = actions;
+    this.pathname = pathname;
+    this.request = request;
+    this.routeData = routeData;
+    this.status = status;
+    this.clientAddress = clientAddress;
+    this.cookies = cookies;
+    this.params = params;
+    this.url = url;
+    this.props = props;
+    this.partial = partial;
+    this.shouldInjectCspMetaTags = shouldInjectCspMetaTags;
+    this.session = session;
+  }
+  /**
+   * A flag that tells the render content if the rewriting was triggered
+   */
+  isRewriting = false;
+  /**
+   * A safety net in case of loops
+   */
+  counter = 0;
+  result = void 0;
+  static async create({
+    locals = {},
+    middleware,
+    pathname,
+    pipeline,
+    request,
+    routeData,
+    clientAddress,
+    status = 200,
+    props,
+    partial = void 0,
+    actions,
+    shouldInjectCspMetaTags
+  }) {
+    const pipelineMiddleware = await pipeline.getMiddleware();
+    const pipelineActions = actions ?? await pipeline.getActions();
+    setOriginPathname(
+      request,
+      pathname,
+      pipeline.manifest.trailingSlash,
+      pipeline.manifest.buildFormat
+    );
+    return new RenderContext(
+      pipeline,
+      locals,
+      sequence(...pipeline.internalMiddleware, middleware ?? pipelineMiddleware),
+      pipelineActions,
+      pathname,
+      request,
+      routeData,
+      status,
+      clientAddress,
+      void 0,
+      void 0,
+      void 0,
+      props,
+      partial,
+      shouldInjectCspMetaTags ?? !!pipeline.manifest.csp
+    );
+  }
+  /**
+   * The main function of the RenderContext.
+   *
+   * Use this function to render any route known to Astro.
+   * It attempts to render a route. A route can be a:
+   *
+   * - page
+   * - redirect
+   * - endpoint
+   * - fallback
+   */
+  async render(componentInstance, slots = {}) {
+    const { middleware, pipeline } = this;
+    const { logger, serverLike, streaming, manifest } = pipeline;
+    const props = Object.keys(this.props).length > 0 ? this.props : await getProps({
+      mod: componentInstance,
+      routeData: this.routeData,
+      routeCache: this.pipeline.routeCache,
+      pathname: this.pathname,
+      logger,
+      serverLike,
+      base: manifest.base
+    });
+    const actionApiContext = this.createActionAPIContext();
+    const apiContext = this.createAPIContext(props, actionApiContext);
+    this.counter++;
+    if (this.counter === 4) {
+      return new Response("Loop Detected", {
+        // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508
+        status: 508,
+        statusText: "Astro detected a loop where you tried to call the rewriting logic more than four times."
+      });
+    }
+    const lastNext = async (ctx, payload) => {
+      if (payload) {
+        const oldPathname = this.pathname;
+        pipeline.logger.debug("router", "Called rewriting to:", payload);
+        const {
+          routeData,
+          componentInstance: newComponent,
+          pathname,
+          newUrl
+        } = await pipeline.tryRewrite(payload, this.request);
+        if (this.pipeline.serverLike === true && this.routeData.prerender === false && routeData.prerender === true) {
+          throw new AstroError({
+            ...ForbiddenRewrite,
+            message: ForbiddenRewrite.message(this.pathname, pathname, routeData.component),
+            hint: ForbiddenRewrite.hint(routeData.component)
+          });
+        }
+        this.routeData = routeData;
+        componentInstance = newComponent;
+        if (payload instanceof Request) {
+          this.request = payload;
+        } else {
+          this.request = copyRequest(
+            newUrl,
+            this.request,
+            // need to send the flag of the previous routeData
+            routeData.prerender,
+            this.pipeline.logger,
+            this.routeData.route
+          );
+        }
+        this.isRewriting = true;
+        this.url = new URL(this.request.url);
+        this.params = getParams(routeData, pathname);
+        this.pathname = pathname;
+        this.status = 200;
+        setOriginPathname(
+          this.request,
+          oldPathname,
+          this.pipeline.manifest.trailingSlash,
+          this.pipeline.manifest.buildFormat
+        );
+      }
+      let response2;
+      if (!ctx.isPrerendered) {
+        const { action, setActionResult, serializeActionResult } = getActionContext(ctx);
+        if (action?.calledFrom === "form") {
+          const actionResult = await action.handler();
+          setActionResult(action.name, serializeActionResult(actionResult));
+        }
+      }
+      switch (this.routeData.type) {
+        case "endpoint": {
+          response2 = await renderEndpoint(
+            componentInstance,
+            ctx,
+            this.routeData.prerender,
+            logger
+          );
+          break;
+        }
+        case "redirect":
+          return renderRedirect(this);
+        case "page": {
+          this.result = await this.createResult(componentInstance, actionApiContext);
+          try {
+            response2 = await renderPage(
+              this.result,
+              componentInstance?.default,
+              props,
+              slots,
+              streaming,
+              this.routeData
+            );
+          } catch (e) {
+            this.result.cancelled = true;
+            throw e;
+          }
+          response2.headers.set(ROUTE_TYPE_HEADER, "page");
+          if (this.routeData.route === "/404" || this.routeData.route === "/500") {
+            response2.headers.set(REROUTE_DIRECTIVE_HEADER, "no");
+          }
+          if (this.isRewriting) {
+            response2.headers.set(REWRITE_DIRECTIVE_HEADER_KEY, REWRITE_DIRECTIVE_HEADER_VALUE);
+          }
+          break;
+        }
+        case "fallback": {
+          return new Response(null, { status: 500, headers: { [ROUTE_TYPE_HEADER]: "fallback" } });
+        }
+      }
+      const responseCookies = getCookiesFromResponse(response2);
+      if (responseCookies) {
+        this.cookies.merge(responseCookies);
+      }
+      return response2;
+    };
+    if (isRouteExternalRedirect(this.routeData)) {
+      return renderRedirect(this);
+    }
+    const response = await callMiddleware(middleware, apiContext, lastNext);
+    if (response.headers.get(ROUTE_TYPE_HEADER)) {
+      response.headers.delete(ROUTE_TYPE_HEADER);
+    }
+    attachCookiesToResponse(response, this.cookies);
+    return response;
+  }
+  createAPIContext(props, context) {
+    const redirect = (path, status = 302) => new Response(null, { status, headers: { Location: path } });
+    Reflect.set(context, apiContextRoutesSymbol, this.pipeline);
+    return Object.assign(context, {
+      props,
+      redirect,
+      getActionResult: createGetActionResult(context.locals),
+      callAction: createCallAction(context)
+    });
+  }
+  async #executeRewrite(reroutePayload) {
+    this.pipeline.logger.debug("router", "Calling rewrite: ", reroutePayload);
+    const oldPathname = this.pathname;
+    const { routeData, componentInstance, newUrl, pathname } = await this.pipeline.tryRewrite(
+      reroutePayload,
+      this.request
+    );
+    const isI18nFallback = routeData.fallbackRoutes && routeData.fallbackRoutes.length > 0;
+    if (this.pipeline.serverLike && !this.routeData.prerender && routeData.prerender && !isI18nFallback) {
+      throw new AstroError({
+        ...ForbiddenRewrite,
+        message: ForbiddenRewrite.message(this.pathname, pathname, routeData.component),
+        hint: ForbiddenRewrite.hint(routeData.component)
+      });
+    }
+    this.routeData = routeData;
+    if (reroutePayload instanceof Request) {
+      this.request = reroutePayload;
+    } else {
+      this.request = copyRequest(
+        newUrl,
+        this.request,
+        // need to send the flag of the previous routeData
+        routeData.prerender,
+        this.pipeline.logger,
+        this.routeData.route
+      );
+    }
+    this.url = new URL(this.request.url);
+    const newCookies = new AstroCookies(this.request);
+    if (this.cookies) {
+      newCookies.merge(this.cookies);
+    }
+    this.cookies = newCookies;
+    this.params = getParams(routeData, pathname);
+    this.pathname = pathname;
+    this.isRewriting = true;
+    this.status = 200;
+    setOriginPathname(
+      this.request,
+      oldPathname,
+      this.pipeline.manifest.trailingSlash,
+      this.pipeline.manifest.buildFormat
+    );
+    return await this.render(componentInstance);
+  }
+  createActionAPIContext() {
+    const renderContext = this;
+    const { params, pipeline, url } = this;
+    const generator = `Astro v${ASTRO_VERSION}`;
+    const rewrite = async (reroutePayload) => {
+      return await this.#executeRewrite(reroutePayload);
+    };
+    return {
+      // Don't allow reassignment of cookies because it doesn't work
+      get cookies() {
+        return renderContext.cookies;
+      },
+      routePattern: this.routeData.route,
+      isPrerendered: this.routeData.prerender,
+      get clientAddress() {
+        return renderContext.getClientAddress();
+      },
+      get currentLocale() {
+        return renderContext.computeCurrentLocale();
+      },
+      generator,
+      get locals() {
+        return renderContext.locals;
+      },
+      set locals(_) {
+        throw new AstroError(LocalsReassigned);
+      },
+      params,
+      get preferredLocale() {
+        return renderContext.computePreferredLocale();
+      },
+      get preferredLocaleList() {
+        return renderContext.computePreferredLocaleList();
+      },
+      rewrite,
+      request: this.request,
+      site: pipeline.site,
+      url,
+      get originPathname() {
+        return getOriginPathname(renderContext.request);
+      },
+      get session() {
+        if (this.isPrerendered) {
+          pipeline.logger.warn(
+            "session",
+            `context.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered routes. If you need access to sessions, make sure that the route is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your routes server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`
+          );
+          return void 0;
+        }
+        if (!renderContext.session) {
+          pipeline.logger.warn(
+            "session",
+            `context.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`
+          );
+          return void 0;
+        }
+        return renderContext.session;
+      },
+      get csp() {
+        return {
+          insertDirective(payload) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.directives.push(payload);
+          },
+          insertScriptResource(resource) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.scriptResources.push(resource);
+          },
+          insertStyleResource(resource) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.styleResources.push(resource);
+          },
+          insertStyleHash(hash) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.styleHashes.push(hash);
+          },
+          insertScriptHash(hash) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.scriptHashes.push(hash);
+          }
+        };
+      }
+    };
+  }
+  async createResult(mod, ctx) {
+    const { cookies, pathname, pipeline, routeData, status } = this;
+    const { clientDirectives, inlinedScripts, compressHTML, manifest, renderers, resolve } = pipeline;
+    const { links, scripts, styles } = await pipeline.headElements(routeData);
+    const extraStyleHashes = [];
+    const extraScriptHashes = [];
+    const shouldInjectCspMetaTags = this.shouldInjectCspMetaTags;
+    const cspAlgorithm = manifest.csp?.algorithm ?? "SHA-256";
+    if (shouldInjectCspMetaTags) {
+      for (const style of styles) {
+        extraStyleHashes.push(await generateCspDigest(style.children, cspAlgorithm));
+      }
+      for (const script of scripts) {
+        extraScriptHashes.push(await generateCspDigest(script.children, cspAlgorithm));
+      }
+    }
+    const componentMetadata = await pipeline.componentMetadata(routeData) ?? manifest.componentMetadata;
+    const headers = new Headers({ "Content-Type": "text/html" });
+    const partial = typeof this.partial === "boolean" ? this.partial : Boolean(mod.partial);
+    const actionResult = hasActionPayload(this.locals) ? deserializeActionResult(this.locals._actionPayload.actionResult) : void 0;
+    const response = {
+      status: actionResult?.error ? actionResult?.error.status : status,
+      statusText: actionResult?.error ? actionResult?.error.type : "OK",
+      get headers() {
+        return headers;
+      },
+      // Disallow `Astro.response.headers = new Headers`
+      set headers(_) {
+        throw new AstroError(AstroResponseHeadersReassigned);
+      }
+    };
+    const result = {
+      base: manifest.base,
+      userAssetsBase: manifest.userAssetsBase,
+      cancelled: false,
+      clientDirectives,
+      inlinedScripts,
+      componentMetadata,
+      compressHTML,
+      cookies,
+      /** This function returns the `Astro` faux-global */
+      createAstro: (astroGlobal, props, slots) => this.createAstro(result, astroGlobal, props, slots, ctx),
+      links,
+      params: this.params,
+      partial,
+      pathname,
+      renderers,
+      resolve,
+      response,
+      request: this.request,
+      scripts,
+      styles,
+      actionResult,
+      serverIslandNameMap: manifest.serverIslandNameMap ?? /* @__PURE__ */ new Map(),
+      key: manifest.key,
+      trailingSlash: manifest.trailingSlash,
+      _metadata: {
+        hasHydrationScript: false,
+        rendererSpecificHydrationScripts: /* @__PURE__ */ new Set(),
+        hasRenderedHead: false,
+        renderedScripts: /* @__PURE__ */ new Set(),
+        hasDirectives: /* @__PURE__ */ new Set(),
+        hasRenderedServerIslandRuntime: false,
+        headInTree: false,
+        extraHead: [],
+        extraStyleHashes,
+        extraScriptHashes,
+        propagators: /* @__PURE__ */ new Set()
+      },
+      cspDestination: manifest.csp?.cspDestination ?? (routeData.prerender ? "meta" : "header"),
+      shouldInjectCspMetaTags,
+      cspAlgorithm,
+      // The following arrays must be cloned, otherwise they become mutable across routes.
+      scriptHashes: manifest.csp?.scriptHashes ? [...manifest.csp.scriptHashes] : [],
+      scriptResources: manifest.csp?.scriptResources ? [...manifest.csp.scriptResources] : [],
+      styleHashes: manifest.csp?.styleHashes ? [...manifest.csp.styleHashes] : [],
+      styleResources: manifest.csp?.styleResources ? [...manifest.csp.styleResources] : [],
+      directives: manifest.csp?.directives ? [...manifest.csp.directives] : [],
+      isStrictDynamic: manifest.csp?.isStrictDynamic ?? false
+    };
+    return result;
+  }
+  #astroPagePartial;
+  /**
+   * The Astro global is sourced in 3 different phases:
+   * - **Static**: `.generator` and `.glob` is printed by the compiler, instantiated once per process per astro file
+   * - **Page-level**: `.request`, `.cookies`, `.locals` etc. These remain the same for the duration of the request.
+   * - **Component-level**: `.props`, `.slots`, and `.self` are unique to each _use_ of each component.
+   *
+   * The page level partial is used as the prototype of the user-visible `Astro` global object, which is instantiated once per use of a component.
+   */
+  createAstro(result, astroStaticPartial, props, slotValues, apiContext) {
+    let astroPagePartial;
+    if (this.isRewriting) {
+      astroPagePartial = this.#astroPagePartial = this.createAstroPagePartial(
+        result,
+        astroStaticPartial,
+        apiContext
+      );
+    } else {
+      astroPagePartial = this.#astroPagePartial ??= this.createAstroPagePartial(
+        result,
+        astroStaticPartial,
+        apiContext
+      );
+    }
+    const astroComponentPartial = { props, self: null };
+    const Astro = Object.assign(
+      Object.create(astroPagePartial),
+      astroComponentPartial
+    );
+    let _slots;
+    Object.defineProperty(Astro, "slots", {
+      get: () => {
+        if (!_slots) {
+          _slots = new Slots(
+            result,
+            slotValues,
+            this.pipeline.logger
+          );
+        }
+        return _slots;
+      }
+    });
+    return Astro;
+  }
+  createAstroPagePartial(result, astroStaticPartial, apiContext) {
+    const renderContext = this;
+    const { cookies, locals, params, pipeline, url } = this;
+    const { response } = result;
+    const redirect = (path, status = 302) => {
+      if (this.request[responseSentSymbol$1]) {
+        throw new AstroError({
+          ...ResponseSentError
+        });
+      }
+      return new Response(null, { status, headers: { Location: path } });
+    };
+    const rewrite = async (reroutePayload) => {
+      return await this.#executeRewrite(reroutePayload);
+    };
+    const callAction = createCallAction(apiContext);
+    return {
+      generator: astroStaticPartial.generator,
+      glob: astroStaticPartial.glob,
+      routePattern: this.routeData.route,
+      isPrerendered: this.routeData.prerender,
+      cookies,
+      get session() {
+        if (this.isPrerendered) {
+          pipeline.logger.warn(
+            "session",
+            `Astro.session was used when rendering the route ${green(this.routePattern)}, but it is not available on prerendered pages. If you need access to sessions, make sure that the page is server-rendered using \`export const prerender = false;\` or by setting \`output\` to \`"server"\` in your Astro config to make all your pages server-rendered by default. For more information, see https://docs.astro.build/en/guides/sessions/`
+          );
+          return void 0;
+        }
+        if (!renderContext.session) {
+          pipeline.logger.warn(
+            "session",
+            `Astro.session was used when rendering the route ${green(this.routePattern)}, but no storage configuration was provided. Either configure the storage manually or use an adapter that provides session storage. For more information, see https://docs.astro.build/en/guides/sessions/`
+          );
+          return void 0;
+        }
+        return renderContext.session;
+      },
+      get clientAddress() {
+        return renderContext.getClientAddress();
+      },
+      get currentLocale() {
+        return renderContext.computeCurrentLocale();
+      },
+      params,
+      get preferredLocale() {
+        return renderContext.computePreferredLocale();
+      },
+      get preferredLocaleList() {
+        return renderContext.computePreferredLocaleList();
+      },
+      locals,
+      redirect,
+      rewrite,
+      request: this.request,
+      response,
+      site: pipeline.site,
+      getActionResult: createGetActionResult(locals),
+      get callAction() {
+        return callAction;
+      },
+      url,
+      get originPathname() {
+        return getOriginPathname(renderContext.request);
+      },
+      get csp() {
+        return {
+          insertDirective(payload) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.directives.push(payload);
+          },
+          insertScriptResource(resource) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.scriptResources.push(resource);
+          },
+          insertStyleResource(resource) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.styleResources.push(resource);
+          },
+          insertStyleHash(hash) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.styleHashes.push(hash);
+          },
+          insertScriptHash(hash) {
+            if (!pipeline.manifest.csp) {
+              throw new AstroError(CspNotEnabled);
+            }
+            renderContext.result?.scriptHashes.push(hash);
+          }
+        };
+      }
+    };
+  }
+  getClientAddress() {
+    const { pipeline, request, routeData, clientAddress } = this;
+    if (routeData.prerender) {
+      throw new AstroError({
+        ...PrerenderClientAddressNotAvailable,
+        message: PrerenderClientAddressNotAvailable.message(routeData.component)
+      });
+    }
+    if (clientAddress) {
+      return clientAddress;
+    }
+    if (clientAddressSymbol in request) {
+      return Reflect.get(request, clientAddressSymbol);
+    }
+    if (pipeline.adapterName) {
+      throw new AstroError({
+        ...ClientAddressNotAvailable,
+        message: ClientAddressNotAvailable.message(pipeline.adapterName)
+      });
+    }
+    throw new AstroError(StaticClientAddressNotAvailable);
+  }
+  /**
+   * API Context may be created multiple times per request, i18n data needs to be computed only once.
+   * So, it is computed and saved here on creation of the first APIContext and reused for later ones.
+   */
+  #currentLocale;
+  computeCurrentLocale() {
+    const {
+      url,
+      pipeline: { i18n },
+      routeData
+    } = this;
+    if (!i18n) return;
+    const { defaultLocale, locales, strategy } = i18n;
+    const fallbackTo = strategy === "pathname-prefix-other-locales" || strategy === "domains-prefix-other-locales" ? defaultLocale : void 0;
+    if (this.#currentLocale) {
+      return this.#currentLocale;
+    }
+    let computedLocale;
+    if (isRouteServerIsland(routeData)) {
+      let referer = this.request.headers.get("referer");
+      if (referer) {
+        if (URL.canParse(referer)) {
+          referer = new URL(referer).pathname;
+        }
+        computedLocale = computeCurrentLocale(referer, locales, defaultLocale);
+      }
+    } else {
+      let pathname = routeData.pathname;
+      if (!routeData.pattern.test(url.pathname)) {
+        for (const fallbackRoute of routeData.fallbackRoutes) {
+          if (fallbackRoute.pattern.test(url.pathname)) {
+            pathname = fallbackRoute.pathname;
+            break;
+          }
+        }
+      }
+      pathname = pathname && !isRoute404or500(routeData) ? pathname : url.pathname;
+      computedLocale = computeCurrentLocale(pathname, locales, defaultLocale);
+    }
+    this.#currentLocale = computedLocale ?? fallbackTo;
+    return this.#currentLocale;
+  }
+  #preferredLocale;
+  computePreferredLocale() {
+    const {
+      pipeline: { i18n },
+      request
+    } = this;
+    if (!i18n) return;
+    return this.#preferredLocale ??= computePreferredLocale(request, i18n.locales);
+  }
+  #preferredLocaleList;
+  computePreferredLocaleList() {
+    const {
+      pipeline: { i18n },
+      request
+    } = this;
+    if (!i18n) return;
+    return this.#preferredLocaleList ??= computePreferredLocaleList(request, i18n.locales);
+  }
+}
+
+function sequence(...handlers) {
+  const filtered = handlers.filter((h) => !!h);
+  const length = filtered.length;
+  if (!length) {
+    return defineMiddleware((_context, next) => {
+      return next();
+    });
+  }
+  return defineMiddleware((context, next) => {
+    let carriedPayload = void 0;
+    return applyHandle(0, context);
+    function applyHandle(i, handleContext) {
+      const handle = filtered[i];
+      const result = handle(handleContext, async (payload) => {
+        if (i < length - 1) {
+          if (payload) {
+            let newRequest;
+            if (payload instanceof Request) {
+              newRequest = payload;
+            } else if (payload instanceof URL) {
+              newRequest = new Request(payload, handleContext.request.clone());
+            } else {
+              newRequest = new Request(
+                new URL(payload, handleContext.url.origin),
+                handleContext.request.clone()
+              );
+            }
+            const oldPathname = handleContext.url.pathname;
+            const pipeline = Reflect.get(handleContext, apiContextRoutesSymbol);
+            const { routeData, pathname } = await pipeline.tryRewrite(
+              payload,
+              handleContext.request
+            );
+            if (pipeline.serverLike === true && handleContext.isPrerendered === false && routeData.prerender === true) {
+              throw new AstroError({
+                ...ForbiddenRewrite,
+                message: ForbiddenRewrite.message(
+                  handleContext.url.pathname,
+                  pathname,
+                  routeData.component
+                ),
+                hint: ForbiddenRewrite.hint(routeData.component)
+              });
+            }
+            carriedPayload = payload;
+            handleContext.request = newRequest;
+            handleContext.url = new URL(newRequest.url);
+            handleContext.params = getParams(routeData, pathname);
+            handleContext.routePattern = routeData.route;
+            setOriginPathname(
+              handleContext.request,
+              oldPathname,
+              pipeline.manifest.trailingSlash,
+              pipeline.manifest.buildFormat
+            );
+          }
+          return applyHandle(i + 1, handleContext);
+        } else {
+          return next(payload ?? carriedPayload);
+        }
+      });
+      return result;
+    }
+  });
+}
+
+function defineMiddleware(fn) {
+  return fn;
+}
+
+export { PERSIST_SYMBOL as P, RouteCache as R, SERVER_ISLAND_COMPONENT as S, redirectToFallback as a, redirectToDefaultLocale as b, requestHasLocale as c, normalizeTheLocale as d, defineMiddleware as e, SERVER_ISLAND_ROUTE as f, createEndpoint as g, findRouteToRewrite as h, isRequestServerIsland as i, RenderContext as j, getSetCookiesFromResponse as k, matchRoute as m, notFound as n, requestIs404Or500 as r, sequence as s };

+ 10 - 0
dist/_worker.js/chunks/noop-middleware_C9xlyid3.mjs

@@ -0,0 +1,10 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { ap as NOOP_MIDDLEWARE_HEADER } from './astro/server_WO3f6Mge.mjs';
+
+const NOOP_MIDDLEWARE_FN = async (_ctx, next) => {
+  const response = await next();
+  response.headers.set(NOOP_MIDDLEWARE_HEADER, "true");
+  return response;
+};
+
+export { NOOP_MIDDLEWARE_FN as N };

+ 106 - 0
dist/_worker.js/chunks/path_CH3auf61.mjs

@@ -0,0 +1,106 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+function appendForwardSlash(path) {
+  return path.endsWith("/") ? path : path + "/";
+}
+function prependForwardSlash(path) {
+  return path[0] === "/" ? path : "/" + path;
+}
+const MANY_TRAILING_SLASHES = /\/{2,}$/g;
+function collapseDuplicateTrailingSlashes(path, trailingSlash) {
+  if (!path) {
+    return path;
+  }
+  return path.replace(MANY_TRAILING_SLASHES, trailingSlash ? "/" : "") || "/";
+}
+function removeTrailingForwardSlash(path) {
+  return path.endsWith("/") ? path.slice(0, path.length - 1) : path;
+}
+function removeLeadingForwardSlash(path) {
+  return path.startsWith("/") ? path.substring(1) : path;
+}
+function trimSlashes(path) {
+  return path.replace(/^\/|\/$/g, "");
+}
+function isString(path) {
+  return typeof path === "string" || path instanceof String;
+}
+const INTERNAL_PREFIXES = /* @__PURE__ */ new Set(["/_", "/@", "/.", "//"]);
+const JUST_SLASHES = /^\/{2,}$/;
+function isInternalPath(path) {
+  return INTERNAL_PREFIXES.has(path.slice(0, 2)) && !JUST_SLASHES.test(path);
+}
+function joinPaths(...paths) {
+  return paths.filter(isString).map((path, i) => {
+    if (i === 0) {
+      return removeTrailingForwardSlash(path);
+    } else if (i === paths.length - 1) {
+      return removeLeadingForwardSlash(path);
+    } else {
+      return trimSlashes(path);
+    }
+  }).join("/");
+}
+function isRemotePath(src) {
+  if (!src) return false;
+  const trimmed = src.trim();
+  if (!trimmed) return false;
+  let decoded = trimmed;
+  let previousDecoded = "";
+  let maxIterations = 10;
+  while (decoded !== previousDecoded && maxIterations > 0) {
+    previousDecoded = decoded;
+    try {
+      decoded = decodeURIComponent(decoded);
+    } catch {
+      break;
+    }
+    maxIterations--;
+  }
+  if (/^[a-zA-Z]:/.test(decoded)) {
+    return false;
+  }
+  if (decoded[0] === "/" && decoded[1] !== "/" && decoded[1] !== "\\") {
+    return false;
+  }
+  if (decoded[0] === "\\") {
+    return true;
+  }
+  if (decoded.startsWith("//")) {
+    return true;
+  }
+  try {
+    const url = new URL(decoded, "http://n");
+    if (url.username || url.password) {
+      return true;
+    }
+    if (decoded.includes("@") && !url.pathname.includes("@") && !url.search.includes("@")) {
+      return true;
+    }
+    if (url.origin !== "http://n") {
+      const protocol = url.protocol.toLowerCase();
+      if (protocol === "file:") {
+        return false;
+      }
+      return true;
+    }
+    if (URL.canParse(decoded)) {
+      return true;
+    }
+    return false;
+  } catch {
+    return true;
+  }
+}
+function slash(path) {
+  return path.replace(/\\/g, "/");
+}
+function fileExtension(path) {
+  const ext = path.split(".").pop();
+  return ext !== path ? `.${ext}` : "";
+}
+const WITH_FILE_EXT = /\/[^/]+\.\w+$/;
+function hasFileExtension(path) {
+  return WITH_FILE_EXT.test(path);
+}
+
+export { appendForwardSlash as a, isInternalPath as b, collapseDuplicateTrailingSlashes as c, fileExtension as f, hasFileExtension as h, isRemotePath as i, joinPaths as j, prependForwardSlash as p, removeTrailingForwardSlash as r, slash as s, trimSlashes as t };

Fichier diff supprimé car celui-ci est trop grand
+ 218 - 0
dist/_worker.js/chunks/registry_B7C18axh.mjs


+ 58 - 0
dist/_worker.js/chunks/remote_BC1y8RCW.mjs

@@ -0,0 +1,58 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+function matchPattern(url, remotePattern) {
+  return matchProtocol(url, remotePattern.protocol) && matchHostname(url, remotePattern.hostname, true) && matchPort(url, remotePattern.port) && matchPathname(url, remotePattern.pathname, true);
+}
+function matchPort(url, port) {
+  return !port || port === url.port;
+}
+function matchProtocol(url, protocol) {
+  return !protocol || protocol === url.protocol.slice(0, -1);
+}
+function matchHostname(url, hostname, allowWildcard = false) {
+  if (!hostname) {
+    return true;
+  } else if (!allowWildcard || !hostname.startsWith("*")) {
+    return hostname === url.hostname;
+  } else if (hostname.startsWith("**.")) {
+    const slicedHostname = hostname.slice(2);
+    return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
+  } else if (hostname.startsWith("*.")) {
+    const slicedHostname = hostname.slice(1);
+    const additionalSubdomains = url.hostname.replace(slicedHostname, "").split(".").filter(Boolean);
+    return additionalSubdomains.length === 1;
+  }
+  return false;
+}
+function matchPathname(url, pathname, allowWildcard = false) {
+  if (!pathname) {
+    return true;
+  } else if (!allowWildcard || !pathname.endsWith("*")) {
+    return pathname === url.pathname;
+  } else if (pathname.endsWith("/**")) {
+    const slicedPathname = pathname.slice(0, -2);
+    return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
+  } else if (pathname.endsWith("/*")) {
+    const slicedPathname = pathname.slice(0, -1);
+    const additionalPathChunks = url.pathname.replace(slicedPathname, "").split("/").filter(Boolean);
+    return additionalPathChunks.length === 1;
+  }
+  return false;
+}
+function isRemoteAllowed(src, {
+  domains,
+  remotePatterns
+}) {
+  if (!URL.canParse(src)) {
+    return false;
+  }
+  const url = new URL(src);
+  if (url.protocol === "data:") {
+    return true;
+  }
+  if (!["http:", "https:"].includes(url.protocol)) {
+    return false;
+  }
+  return domains.some((domain) => matchHostname(url, domain)) || remotePatterns.some((remotePattern) => matchPattern(url, remotePattern));
+}
+
+export { isRemoteAllowed as i, matchPattern as m };

+ 100 - 0
dist/_worker.js/chunks/sharp_CG4EIo5e.mjs

@@ -0,0 +1,100 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { A as AstroError, at as MissingSharp } from './astro/server_WO3f6Mge.mjs';
+import { b as baseService, p as parseQuality } from './image-endpoint_DVs7qPGs.mjs';
+
+let sharp;
+const qualityTable = {
+  low: 25,
+  mid: 50,
+  high: 80,
+  max: 100
+};
+async function loadSharp() {
+  let sharpImport;
+  try {
+    sharpImport = (await import('sharp')).default;
+  } catch {
+    throw new AstroError(MissingSharp);
+  }
+  sharpImport.cache(false);
+  return sharpImport;
+}
+const fitMap = {
+  fill: "fill",
+  contain: "inside",
+  cover: "cover",
+  none: "outside",
+  "scale-down": "inside",
+  outside: "outside",
+  inside: "inside"
+};
+const sharpService = {
+  validateOptions: baseService.validateOptions,
+  getURL: baseService.getURL,
+  parseURL: baseService.parseURL,
+  getHTMLAttributes: baseService.getHTMLAttributes,
+  getSrcSet: baseService.getSrcSet,
+  async transform(inputBuffer, transformOptions, config) {
+    if (!sharp) sharp = await loadSharp();
+    const transform = transformOptions;
+    if (transform.format === "svg") return { data: inputBuffer, format: "svg" };
+    const result = sharp(inputBuffer, {
+      failOnError: false,
+      pages: -1,
+      limitInputPixels: config.service.config.limitInputPixels
+    });
+    result.rotate();
+    const withoutEnlargement = Boolean(transform.fit);
+    if (transform.width && transform.height && transform.fit) {
+      const fit = fitMap[transform.fit] ?? "inside";
+      result.resize({
+        width: Math.round(transform.width),
+        height: Math.round(transform.height),
+        fit,
+        position: transform.position,
+        withoutEnlargement
+      });
+    } else if (transform.height && !transform.width) {
+      result.resize({
+        height: Math.round(transform.height),
+        withoutEnlargement
+      });
+    } else if (transform.width) {
+      result.resize({
+        width: Math.round(transform.width),
+        withoutEnlargement
+      });
+    }
+    if (transform.format) {
+      let quality = void 0;
+      if (transform.quality) {
+        const parsedQuality = parseQuality(transform.quality);
+        if (typeof parsedQuality === "number") {
+          quality = parsedQuality;
+        } else {
+          quality = transform.quality in qualityTable ? qualityTable[transform.quality] : void 0;
+        }
+      }
+      const isGifInput = inputBuffer[0] === 71 && // 'G'
+      inputBuffer[1] === 73 && // 'I'
+      inputBuffer[2] === 70 && // 'F'
+      inputBuffer[3] === 56 && // '8'
+      (inputBuffer[4] === 57 || inputBuffer[4] === 55) && // '9' or '7'
+      inputBuffer[5] === 97;
+      if (transform.format === "webp" && isGifInput) {
+        result.webp({ quality: typeof quality === "number" ? quality : void 0, loop: 0 });
+      } else {
+        result.toFormat(transform.format, { quality });
+      }
+    }
+    const { data, info } = await result.toBuffer({ resolveWithObject: true });
+    const needsCopy = "buffer" in data && data.buffer instanceof SharedArrayBuffer;
+    return {
+      data: needsCopy ? new Uint8Array(data) : data,
+      format: info.format
+    };
+  }
+};
+var sharp_default = sharpService;
+
+export { sharp_default as default };

+ 68 - 0
dist/_worker.js/chunks/shortlink_FrzJzDij.mjs

@@ -0,0 +1,68 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import require$$0 from '.prisma/client/default';
+
+/* @ts-self-types="./index.d.ts" */
+let random = bytes => crypto.getRandomValues(new Uint8Array(bytes));
+let customRandom = (alphabet, defaultSize, getRandom) => {
+  let mask = (2 << Math.log2(alphabet.length - 1)) - 1;
+  let step = -~((1.6 * mask * defaultSize) / alphabet.length);
+  return (size = defaultSize) => {
+    let id = '';
+    while (true) {
+      let bytes = getRandom(step);
+      let j = step | 0;
+      while (j--) {
+        id += alphabet[bytes[j] & mask] || '';
+        if (id.length >= size) return id
+      }
+    }
+  }
+};
+let customAlphabet = (alphabet, size = 21) =>
+  customRandom(alphabet, size | 0, random);
+
+var _default;
+var hasRequired_default;
+
+function require_default () {
+	if (hasRequired_default) return _default;
+	hasRequired_default = 1;
+	_default = {
+	  ...require$$0,
+	};
+	return _default;
+}
+
+var _defaultExports = /*@__PURE__*/ require_default();
+
+const globalForPrisma = globalThis;
+const prisma = globalForPrisma.prisma ?? new _defaultExports.PrismaClient();
+
+const nanoid = customAlphabet("23456789abcdefghijkmnpqrstuvwxyz", 6);
+async function generateShortLink(fullUrl) {
+  let shortCode = "";
+  let exists = true;
+  while (exists) {
+    shortCode = nanoid();
+    const existing = await prisma.shortLink.findUnique({
+      where: { shortCode }
+    });
+    exists = !!existing;
+  }
+  await prisma.shortLink.create({
+    data: {
+      shortCode,
+      fullUrl
+    }
+  });
+  const domain = process.env.DOMAIN || "localhost:4321";
+  return `${domain}/${shortCode}`;
+}
+async function resolveShortLink(shortCode) {
+  const link = await prisma.shortLink.findUnique({
+    where: { shortCode }
+  });
+  return link?.fullUrl || null;
+}
+
+export { generateShortLink as g, resolveShortLink as r };

+ 40 - 0
dist/_worker.js/index.js

@@ -0,0 +1,40 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { r as renderers } from './chunks/_@astro-renderers_DpxbEuk7.mjs';
+import { c as createExports, s as serverEntrypointModule } from './chunks/_@astrojs-ssr-adapter_DpluC3qk.mjs';
+import { manifest } from './manifest_BP1MTNcW.mjs';
+
+const serverIslandMap = new Map();;
+
+const _page0 = () => import('./pages/_image.astro.mjs');
+const _page1 = () => import('./pages/api/generate-link.astro.mjs');
+const _page2 = () => import('./pages/api/weather.astro.mjs');
+const _page3 = () => import('./pages/app/preview.astro.mjs');
+const _page4 = () => import('./pages/app.astro.mjs');
+const _page5 = () => import('./pages/_shortcode_.astro.mjs');
+const _page6 = () => import('./pages/index.astro.mjs');
+const pageMap = new Map([
+    ["node_modules/@astrojs/cloudflare/dist/entrypoints/image-endpoint.js", _page0],
+    ["src/pages/api/generate-link.ts", _page1],
+    ["src/pages/api/weather.ts", _page2],
+    ["src/pages/app/preview.astro", _page3],
+    ["src/pages/app/index.astro", _page4],
+    ["src/pages/[shortcode].astro", _page5],
+    ["src/pages/index.astro", _page6]
+]);
+
+const _manifest = Object.assign(manifest, {
+    pageMap,
+    serverIslandMap,
+    renderers,
+    actions: () => import('./_noop-actions.mjs'),
+    middleware: () => import('./_astro-internal_middleware.mjs')
+});
+const _args = undefined;
+const _exports = createExports(_manifest);
+const __astrojsSsrVirtualEntry = _exports.default;
+const _start = 'start';
+if (Object.prototype.hasOwnProperty.call(serverEntrypointModule, _start)) {
+	serverEntrypointModule[_start](_manifest, _args);
+}
+
+export { __astrojsSsrVirtualEntry as default, pageMap };

Fichier diff supprimé car celui-ci est trop grand
+ 96 - 0
dist/_worker.js/manifest_BP1MTNcW.mjs


+ 3 - 0
dist/_worker.js/pages/_image.astro.mjs

@@ -0,0 +1,3 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+export { a as page } from '../chunks/image-endpoint_DVs7qPGs.mjs';
+export { r as renderers } from '../chunks/_@astro-renderers_DpxbEuk7.mjs';

+ 33 - 0
dist/_worker.js/pages/_shortcode_.astro.mjs

@@ -0,0 +1,33 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { e as createComponent, f as createAstro, l as renderHead, r as renderTemplate } from '../chunks/astro/server_WO3f6Mge.mjs';
+import { r as resolveShortLink } from '../chunks/shortlink_FrzJzDij.mjs';
+export { r as renderers } from '../chunks/_@astro-renderers_DpxbEuk7.mjs';
+
+const $$Astro = createAstro();
+const $$shortcode = createComponent(async ($$result, $$props, $$slots) => {
+  const Astro2 = $$result.createAstro($$Astro, $$props, $$slots);
+  Astro2.self = $$shortcode;
+  const { shortcode } = Astro2.params;
+  if (!shortcode) {
+    return Astro2.redirect("/app");
+  }
+  const fullUrl = await resolveShortLink(shortcode);
+  if (fullUrl) {
+    return Astro2.redirect(fullUrl);
+  }
+  return renderTemplate`<html> <head><title>Link Not Found</title>${renderHead()}</head> <body> <h1>Link not found</h1> <p>This shortcode does not exist.</p> <a href="/app">Go to Dashboard Builder</a> </body></html>`;
+}, "/home/fc/Projects/glance/src/pages/[shortcode].astro", void 0);
+
+const $$file = "/home/fc/Projects/glance/src/pages/[shortcode].astro";
+const $$url = "/[shortcode]";
+
+const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+    __proto__: null,
+    default: $$shortcode,
+    file: $$file,
+    url: $$url
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const page = () => _page;
+
+export { page };

+ 35 - 0
dist/_worker.js/pages/api/generate-link.astro.mjs

@@ -0,0 +1,35 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { g as generateShortLink } from '../../chunks/shortlink_FrzJzDij.mjs';
+export { r as renderers } from '../../chunks/_@astro-renderers_DpxbEuk7.mjs';
+
+const POST = async ({ request }) => {
+  try {
+    const { url } = await request.json();
+    if (!url) {
+      return new Response(JSON.stringify({ error: "URL is required" }), {
+        status: 400,
+        headers: { "Content-Type": "application/json" }
+      });
+    }
+    const shortLink = await generateShortLink(url);
+    return new Response(JSON.stringify({ shortLink }), {
+      status: 200,
+      headers: { "Content-Type": "application/json" }
+    });
+  } catch (error) {
+    console.error("Error generating short link:", error);
+    return new Response(JSON.stringify({ error: "Failed to generate short link" }), {
+      status: 500,
+      headers: { "Content-Type": "application/json" }
+    });
+  }
+};
+
+const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+  __proto__: null,
+  POST
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const page = () => _page;
+
+export { page };

+ 83 - 0
dist/_worker.js/pages/api/weather.astro.mjs

@@ -0,0 +1,83 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+export { r as renderers } from '../../chunks/_@astro-renderers_DpxbEuk7.mjs';
+
+const GET = async ({ request }) => {
+  const url = new URL(request.url);
+  const city = url.searchParams.get("city");
+  const lat = url.searchParams.get("lat");
+  const lon = url.searchParams.get("lon");
+  const apiKey = url.searchParams.get("apiKey");
+  const units = url.searchParams.get("units") || "metric";
+  if (!apiKey) {
+    return new Response(JSON.stringify({ error: "Missing apiKey parameter" }), {
+      status: 400,
+      headers: {
+        "Content-Type": "application/json"
+      }
+    });
+  }
+  if (!city && (!lat || !lon)) {
+    return new Response(
+      JSON.stringify({ error: "Missing city or lat/lon parameters" }),
+      {
+        status: 400,
+        headers: {
+          "Content-Type": "application/json"
+        }
+      }
+    );
+  }
+  try {
+    let weatherUrl;
+    if (lat && lon) {
+      weatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&appid=${apiKey}&units=${units}`;
+    } else {
+      const req = await fetch(
+        `http://api.openweathermap.org/geo/1.0/direct?q=${city}&limit=1&appid=${apiKey}`
+      );
+      const json = await req.json();
+      const lata = json[0]?.lat | 0;
+      const longa = json[0]?.lon | 0;
+      weatherUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${encodeURIComponent(lata)}&lon=${encodeURIComponent(longa)}&appid=${apiKey}&units=${units}`;
+    }
+    const response = await fetch(weatherUrl);
+    if (!response.ok) {
+      console.log(response);
+      return new Response(
+        JSON.stringify({ error: "Failed to fetch weather data" }),
+        {
+          status: response.status,
+          headers: {
+            "Content-Type": "application/json"
+          }
+        }
+      );
+    }
+    const data = await response.json();
+    return new Response(JSON.stringify(data), {
+      status: 200,
+      headers: {
+        "Content-Type": "application/json",
+        "Cache-Control": "public, max-age=300"
+        // Cache for 5 minutes
+      }
+    });
+  } catch (error) {
+    console.error("Weather API error:", error);
+    return new Response(JSON.stringify({ error: "Internal server error" }), {
+      status: 500,
+      headers: {
+        "Content-Type": "application/json"
+      }
+    });
+  }
+};
+
+const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+  __proto__: null,
+  GET
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const page = () => _page;
+
+export { page };

Fichier diff supprimé car celui-ci est trop grand
+ 448 - 0
dist/_worker.js/pages/app.astro.mjs


+ 87 - 0
dist/_worker.js/pages/app/preview.astro.mjs

@@ -0,0 +1,87 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { e as createComponent$1, k as renderComponent, r as renderTemplate } from '../../chunks/astro/server_WO3f6Mge.mjs';
+import { $ as $$Layout } from '../../chunks/Layout_eV-qEGCG.mjs';
+import { c as createMemo, s as ssr, e as escape, a as createComponent, b as ssrHydrationKey, d as ssrStyleProperty, f as createSignal, S as Show, D as Dynamic, F as For } from '../../chunks/_@astro-renderers_DpxbEuk7.mjs';
+export { r as renderers } from '../../chunks/_@astro-renderers_DpxbEuk7.mjs';
+import { g as getGrid, W as WidgetRenderer } from '../../chunks/registry_B7C18axh.mjs';
+
+var _tmpl$ = ["<div", ' style="', '"><h2>Error</h2><p>', "</p></div>"], _tmpl$2 = ["<div", ' data-grid-container style="', '"><!--$-->', "<!--/--><!--$-->", "<!--/--></div>"], _tmpl$3 = ["<div", ' style="', '"><p>No widgets to display</p></div>'], _tmpl$4 = ["<div", ' style="', '"><!--$-->', "<!--/--><!--$-->", "<!--/--><!--$-->", "<!--/--></div>"];
+function Preview() {
+  const [widgets] = createSignal([]);
+  const [gridConfig] = createSignal({
+    templateId: null,
+    widgetPlacements: []
+  });
+  const [error] = createSignal("");
+  const scale = createMemo(() => {
+    const grid = gridConfig().templateId ? getGrid(gridConfig().templateId) : null;
+    if (!grid) return 1;
+    const gridWidth = grid.template?.width || 800;
+    const gridHeight = grid.template?.height || 600;
+    const viewportWidth = window.innerWidth - 40;
+    const viewportHeight = window.innerHeight - 40;
+    const scaleX = viewportWidth / gridWidth;
+    const scaleY = viewportHeight / gridHeight;
+    return Math.min(scaleX, scaleY) * 0.98;
+  });
+  return ssr(_tmpl$4, ssrHydrationKey(), ssrStyleProperty("width:", "100vw") + ssrStyleProperty(";height:", "100vh") + ssrStyleProperty(";position:", "fixed") + ssrStyleProperty(";top:", "0") + ssrStyleProperty(";left:", "0") + ssrStyleProperty(";background:", "var(--bg, #fff)") + ssrStyleProperty(";overflow:", "hidden") + ssrStyleProperty(";display:", "flex") + ssrStyleProperty(";align-items:", "center") + ssrStyleProperty(";justify-content:", "center") + ssrStyleProperty(";padding:", "20px") + ssrStyleProperty(";box-sizing:", "border-box"), escape(createComponent(Show, {
+    get when() {
+      return error();
+    },
+    get children() {
+      return ssr(_tmpl$, ssrHydrationKey(), ssrStyleProperty("padding:", "2rem") + ssrStyleProperty(";text-align:", "center") + ssrStyleProperty(";color:", "var(--gray)"), escape(error()));
+    }
+  })), escape(createComponent(Show, {
+    get when() {
+      return !error() && widgets().length > 0;
+    },
+    get children() {
+      return ssr(_tmpl$2, ssrHydrationKey(), ssrStyleProperty("width:", gridConfig().templateId ? `${escape(getGrid(gridConfig().templateId)?.template.width, true) || 800}px` : "100%") + ssrStyleProperty(";height:", gridConfig().templateId ? `${escape(getGrid(gridConfig().templateId)?.template.height, true) || 600}px` : "100%") + ssrStyleProperty(";position:", "relative") + ssrStyleProperty(";transform:", `scale(${escape(scale(), true)})`) + ssrStyleProperty(";transform-origin:", "center center") + ssrStyleProperty(";flex-shrink:", "0"), escape(createComponent(Show, {
+        get when() {
+          return gridConfig().templateId;
+        },
+        children: () => {
+          const grid = getGrid(gridConfig().templateId);
+          return grid ? createComponent(Dynamic, {
+            get component() {
+              return grid.Component;
+            }
+          }) : null;
+        }
+      })), escape(createComponent(For, {
+        get each() {
+          return widgets();
+        },
+        children: (widget) => createComponent(WidgetRenderer, {
+          config: widget,
+          locked: true
+        })
+      })));
+    }
+  })), escape(createComponent(Show, {
+    get when() {
+      return !error() && widgets().length === 0;
+    },
+    get children() {
+      return ssr(_tmpl$3, ssrHydrationKey(), ssrStyleProperty("padding:", "2rem") + ssrStyleProperty(";text-align:", "center") + ssrStyleProperty(";color:", "var(--gray)"));
+    }
+  })));
+}
+
+const $$Preview = createComponent$1(($$result, $$props, $$slots) => {
+  return renderTemplate`${renderComponent($$result, "Layout", $$Layout, { "title": "Dashboard Preview" }, { "default": ($$result2) => renderTemplate` ${renderComponent($$result2, "Preview", Preview, { "client:load": true, "client:component-hydration": "load", "client:component-path": "/home/fc/Projects/glance/src/components/Preview", "client:component-export": "Preview" })} ` })}`;
+}, "/home/fc/Projects/glance/src/pages/app/preview.astro", void 0);
+
+const $$file = "/home/fc/Projects/glance/src/pages/app/preview.astro";
+const $$url = "/app/preview";
+
+const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+  __proto__: null,
+  default: $$Preview,
+  file: $$file,
+  url: $$url
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const page = () => _page;
+
+export { page };

+ 28 - 0
dist/_worker.js/pages/index.astro.mjs

@@ -0,0 +1,28 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+import { e as createComponent, k as renderComponent, r as renderTemplate, m as maybeRenderHead, n as renderScript } from '../chunks/astro/server_WO3f6Mge.mjs';
+import { $ as $$Layout } from '../chunks/Layout_eV-qEGCG.mjs';
+/* empty css                                 */
+export { r as renderers } from '../chunks/_@astro-renderers_DpxbEuk7.mjs';
+
+const $$Index = createComponent(($$result, $$props, $$slots) => {
+  return renderTemplate`${renderComponent($$result, "Layout", $$Layout, { "title": "dashMaker - Own Your Tech", "data-astro-cid-j7pv25f6": true }, { "default": ($$result2) => renderTemplate` ${maybeRenderHead()}<main data-astro-cid-j7pv25f6> <div class="hero" data-astro-cid-j7pv25f6> <div class="hero-content" data-astro-cid-j7pv25f6> <h1 class="headline" data-astro-cid-j7pv25f6>
+YOUR DASHBOARD.<br data-astro-cid-j7pv25f6> <span class="accent" data-astro-cid-j7pv25f6>YOUR WAY.</span> </h1> <p class="subheadline" data-astro-cid-j7pv25f6>
+A modular e-ink display for what matters to you.<br data-astro-cid-j7pv25f6>
+Open source tools. <span class="tasteful" data-astro-cid-j7pv25f6>Build it yourself or get it ready-made.</span> </p> <p class="manifesto" data-astro-cid-j7pv25f6>
+When did "open" stop meaning open source? Every line of code
+                    on this hardware is truly open.<br data-astro-cid-j7pv25f6> <span class="king" data-astro-cid-j7pv25f6>As <span id="tech-leader" data-astro-cid-j7pv25f6>Linus Torvalds</span> intended.</span> </p> <div class="cta-group" data-astro-cid-j7pv25f6> <a href="#preorder" class="cta-button primary" data-astro-cid-j7pv25f6> <span class="button-text" data-astro-cid-j7pv25f6>ORDER YOURS</span> <span class="arrow" data-astro-cid-j7pv25f6>→</span> </a> <a href="/app" class="cta-button secondary" data-astro-cid-j7pv25f6> <span class="button-text" data-astro-cid-j7pv25f6>BUILD IT YOURSELF</span> <span class="arrow" data-astro-cid-j7pv25f6>→</span> </a> </div> <a href="/app" class="software-link" data-astro-cid-j7pv25f6> <span class="link-icon" data-astro-cid-j7pv25f6>→</span> <span data-astro-cid-j7pv25f6>Try the dashboard builder (free & open source)</span> </a> </div> <div class="canvas-container" data-astro-cid-j7pv25f6> <canvas id="tablet-canvas" data-astro-cid-j7pv25f6></canvas> <div class="floating-text" data-astro-cid-j7pv25f6> <span data-astro-cid-j7pv25f6>OPEN SOURCE</span> <span data-astro-cid-j7pv25f6>MODULAR</span> <span data-astro-cid-j7pv25f6>YOURS</span> </div> </div> </div> <div class="animated-lines" id="lines" data-astro-cid-j7pv25f6></div> </main>  ${renderScript($$result2, "/home/fc/Projects/glance/src/pages/index.astro?astro&type=script&index=0&lang.ts")} ` })}`;
+}, "/home/fc/Projects/glance/src/pages/index.astro", void 0);
+
+const $$file = "/home/fc/Projects/glance/src/pages/index.astro";
+const $$url = "";
+
+const _page = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
+    __proto__: null,
+    default: $$Index,
+    file: $$file,
+    url: $$url
+}, Symbol.toStringTag, { value: 'Module' }));
+
+const page = () => _page;
+
+export { page };

+ 2 - 0
dist/_worker.js/renderers.mjs

@@ -0,0 +1,2 @@
+globalThis.process ??= {}; globalThis.process.env ??= {};
+export { r as renderers } from './chunks/_@astro-renderers_DpxbEuk7.mjs';

+ 9 - 0
dist/favicon.svg

@@ -0,0 +1,9 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
+    <path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
+    <style>
+        path { fill: #000; }
+        @media (prefers-color-scheme: dark) {
+            path { fill: #FFF; }
+        }
+    </style>
+</svg>

Fichier diff supprimé car celui-ci est trop grand
+ 235 - 539
package-lock.json


+ 4 - 0
package.json

@@ -10,6 +10,7 @@
     "astro": "npx astro"
   },
   "dependencies": {
+    "@astrojs/cloudflare": "^12.6.10",
     "@astrojs/node": "^9.4.5",
     "@astrojs/solid-js": "^5.1.1",
     "@neodrag/solid": "^2.3.1",
@@ -20,5 +21,8 @@
     "prisma": "^6.17.1",
     "solid-js": "^1.9.9",
     "three": "^0.180.0"
+  },
+  "devDependencies": {
+    "wrangler": "^4.44.0"
   }
 }

+ 4 - 12
src/components/Dashboard.tsx

@@ -27,7 +27,6 @@ export const Dashboard: Component = () => {
   const [hoveredCellId, setHoveredCellId] = createSignal<string | null>(null);
   const [isDraggingAny, setIsDraggingAny] = createSignal(false);
 
-  // Calculate scale to fit grid inside preview box
   const previewScale = createMemo(() => {
     const grid = gridConfig().templateId
       ? getGrid(gridConfig().templateId!)
@@ -37,19 +36,16 @@ export const Dashboard: Component = () => {
     const gridWidth = grid.template.width || 800;
     const gridHeight = grid.template.height || 600;
 
-    // Account for split view - left panel is now smaller
-    const containerWidth = window.innerWidth - 450 - 140; // Subtract right panel + margins
+    const containerWidth = window.innerWidth - 450 - 140;
     const containerHeight = window.innerHeight * 0.85 - 80;
 
     const scaleX = containerWidth / gridWidth;
     const scaleY = containerHeight / gridHeight;
 
-    // Use slightly smaller scale to ensure nothing clips
     return Math.min(scaleX, scaleY, 1) * 0.9;
   });
 
   const handleAddWidget = (config: WidgetConfig) => {
-    // Initialize position if not set
     if (!config.position) {
       config.position = { x: 10, y: 10 };
     }
@@ -106,13 +102,12 @@ export const Dashboard: Component = () => {
       reader.onload = (event) => {
         try {
           const config = JSON.parse(event.target?.result as string);
-          // Support both old format (array) and new format (object with widgets and grid)
           if (Array.isArray(config)) {
-            // Old format - just widgets
+            // Old format
             setWidgets(config);
             saveToLocalStorage(config);
           } else {
-            // New format with widgets and grid
+            // New format
             if (config.widgets) {
               setWidgets(config.widgets);
               saveToLocalStorage(config.widgets);
@@ -152,7 +147,7 @@ export const Dashboard: Component = () => {
       }
     } catch (error) {
       console.error("Error generating short link:", error);
-      setShortLink(""); // Clear short link on error
+      setShortLink("");
     }
 
     setShowUrlModal(true);
@@ -178,11 +173,9 @@ export const Dashboard: Component = () => {
 
   const handleWidgetSnap = (widgetId: string, cellId: string | null) => {
     const updated = { ...gridConfig() };
-    // Remove existing placement for this widget
     updated.widgetPlacements = updated.widgetPlacements.filter(
       (p) => p.widgetId !== widgetId,
     );
-    // Add new placement if cellId is provided
     if (cellId) {
       updated.widgetPlacements.push({ widgetId, cellId });
     }
@@ -209,7 +202,6 @@ export const Dashboard: Component = () => {
     localStorage.setItem("dashboard-grid-config", JSON.stringify(config));
   };
 
-  // Load from localStorage on mount (client-side only)
   onMount(() => {
     const saved = localStorage.getItem("dashboard-config");
     if (saved) {

+ 0 - 1
src/components/GridConfigurator.tsx

@@ -13,7 +13,6 @@ export function GridConfigurator(props: GridConfiguratorProps) {
 
   const handleSelectGrid = (templateId: string) => {
     if (props.currentGridId === templateId) {
-      // Deselect if clicking the same grid
       props.onSelectGrid(null);
     } else {
       props.onSelectGrid(templateId);

+ 3 - 6
src/components/Preview.tsx

@@ -13,7 +13,6 @@ export function Preview() {
   });
   const [error, setError] = createSignal<string>("");
 
-  // Calculate scale to fit grid inside viewport
   const scale = createMemo(() => {
     const grid = gridConfig().templateId
       ? getGrid(gridConfig().templateId!)
@@ -22,14 +21,13 @@ export function Preview() {
 
     const gridWidth = grid.template?.width || 800;
     const gridHeight = grid.template?.height || 600;
-    const viewportWidth = window.innerWidth - 40; // Add margin for safety
-    const viewportHeight = window.innerHeight - 40; // Add margin for safety
+    const viewportWidth = window.innerWidth - 40;
+    const viewportHeight = window.innerHeight - 40;
 
     const scaleX = viewportWidth / gridWidth;
     const scaleY = viewportHeight / gridHeight;
 
-    // Use slightly smaller scale to ensure nothing clips
-    return Math.min(scaleX, scaleY) * 0.98; // 98% to add buffer
+    return Math.min(scaleX, scaleY) * 0.98;
   });
 
   onMount(() => {
@@ -41,7 +39,6 @@ export function Preview() {
         const decoded = decodeURIComponent(configParam);
         const config = JSON.parse(decoded);
 
-        // Support both old format (array) and new format (object with widgets and grid)
         if (Array.isArray(config)) {
           setWidgets(config);
         } else {

+ 3 - 30
src/components/TabletPreview.tsx

@@ -22,7 +22,6 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
   const createTablet = () => {
     const group = new THREE.Group();
 
-    // Main screen (e-ink display) - LANDSCAPE orientation (wider than tall)
     const screenGeometry = new THREE.PlaneGeometry(6, 4);
     const screenMaterial = new THREE.MeshStandardMaterial({
       color: 0xeeeeee,
@@ -34,14 +33,12 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     screenMesh.position.z = 0.07;
     group.add(screenMesh);
 
-    // Frame parts
     const frameMaterial = new THREE.MeshStandardMaterial({
       color: 0x0a0a0a,
       roughness: 0.6,
       metalness: 0.3,
     });
 
-    // Top frame
     const topFrame = new THREE.Mesh(
       new THREE.BoxGeometry(6.2, 0.2, 0.15),
       frameMaterial,
@@ -50,7 +47,6 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     topFrame.position.z = 0.04;
     group.add(topFrame);
 
-    // Side frames
     const sideFrame = new THREE.Mesh(
       new THREE.BoxGeometry(0.1, 4, 0.15),
       frameMaterial,
@@ -63,7 +59,6 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     rightSideFrame.position.x = 3.05;
     group.add(rightSideFrame);
 
-    // Bottom frame
     const bottomFrame = new THREE.Mesh(
       new THREE.BoxGeometry(6.2, 0.2, 0.15),
       frameMaterial,
@@ -72,7 +67,6 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     bottomFrame.position.z = 0.04;
     group.add(bottomFrame);
 
-    // Back plate
     const backPlate = new THREE.Mesh(
       new THREE.BoxGeometry(6.5, 4.5, 0.12),
       frameMaterial,
@@ -84,19 +78,16 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
   };
 
   const updateScreenTexture = async () => {
-    // Find the dashboard grid container
     const gridContainer = document.querySelector(
       "[data-grid-container]",
     ) as HTMLElement;
     if (!gridContainer || !screen) return;
 
     try {
-      // Get the actual rendered size of the container
       const rect = gridContainer.getBoundingClientRect();
 
-      // Capture the dashboard at its actual display size
       const canvas = await html2canvas(gridContainer, {
-        scale: 1.5, // Good quality without being too heavy
+        scale: 1.5,
         backgroundColor: "#ffffff",
         logging: false,
         useCORS: true,
@@ -106,34 +97,27 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
         windowHeight: rect.height,
       });
 
-      // Create texture from canvas
       const texture = new THREE.CanvasTexture(canvas);
       texture.needsUpdate = true;
 
-      // Ensure texture fills the entire screen properly
       texture.wrapS = THREE.ClampToEdgeWrapping;
       texture.wrapT = THREE.ClampToEdgeWrapping;
       texture.minFilter = THREE.LinearFilter;
       texture.magFilter = THREE.LinearFilter;
 
-      // Calculate aspect ratios to prevent stretching
       const canvasAspect = canvas.width / canvas.height;
-      const screenAspect = 6 / 4; // Tablet screen aspect ratio (1.5)
+      const screenAspect = 6 / 4;
 
-      // Use "contain" logic - fit entire content without overflow
       if (canvasAspect > screenAspect) {
-        // Canvas is wider - scale down to fit width
         const scale = screenAspect / canvasAspect;
         texture.repeat.set(1, 1 / scale);
         texture.offset.set(0, (1 - texture.repeat.y) / 2);
       } else {
-        // Canvas is taller - scale down to fit height
         const scale = canvasAspect / screenAspect;
         texture.repeat.set(1 / scale, 1);
         texture.offset.set((1 - texture.repeat.x) / 2, 0);
       }
 
-      // Update screen material
       const material = screen.material as THREE.MeshStandardMaterial;
       material.map = texture;
       material.needsUpdate = true;
@@ -145,7 +129,6 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
   onMount(() => {
     if (!canvasRef) return;
 
-    // Scene setup
     scene = new THREE.Scene();
     camera = new THREE.PerspectiveCamera(
       50,
@@ -162,13 +145,11 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     renderer.setSize(canvasRef.clientWidth, canvasRef.clientHeight);
     renderer.setClearColor(0x000000, 0);
 
-    // Create tablet
     const tabletResult = createTablet();
     tablet = tabletResult.group;
     screen = tabletResult.screen;
     scene.add(tablet);
 
-    // Lighting
     const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
     scene.add(ambientLight);
 
@@ -180,15 +161,12 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     rimLight.position.set(-3, 0, -3);
     scene.add(rimLight);
 
-    // Camera position - zoomed out more
     camera.position.set(0, 2, 16);
     camera.lookAt(0, 0, 0);
 
-    // Slight tilt for initial view
     tablet.rotation.x = -0.15;
     tablet.rotation.y = 0.08;
 
-    // Add OrbitControls for mouse interaction
     controls = new OrbitControls(camera, canvasRef);
     controls.enableDamping = true;
     controls.dampingFactor = 0.05;
@@ -199,14 +177,11 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
     controls.autoRotate = false;
     controls.rotateSpeed = 0.5;
 
-    // Animation loop
     const animate = () => {
       animationFrameId = requestAnimationFrame(animate);
 
-      // Update controls
       controls.update();
 
-      // Subtle floating animation
       tablet.position.y = Math.sin(Date.now() * 0.0005) * 0.05;
 
       renderer.render(scene, camera);
@@ -214,13 +189,11 @@ export const TabletPreview: Component<TabletPreviewProps> = (props) => {
 
     animate();
 
-    // Update screen texture periodically (every 2 seconds)
-    updateScreenTexture(); // Initial update
+    updateScreenTexture();
     updateInterval = window.setInterval(() => {
       updateScreenTexture();
     }, 2000);
 
-    // Handle window resize
     const handleResize = () => {
       if (!canvasRef) return;
       camera.aspect = canvasRef.clientWidth / canvasRef.clientHeight;

+ 0 - 1
src/components/WidgetConfigurator.tsx

@@ -15,7 +15,6 @@ export function WidgetConfigurator(props: WidgetConfiguratorProps) {
     setSelectedType(type);
     setShowForm(true);
 
-    // Initialize settings with defaults
     const widget = widgetRegistry[type];
     const defaultSettings: WidgetSettings = {};
 

+ 5 - 73
src/components/WidgetRenderer.tsx

@@ -2,6 +2,8 @@ import { getWidget } from "../widgets/registry";
 import { createSignal, Show, onMount } from "solid-js";
 import { Dynamic } from "solid-js/web";
 import { createDraggable } from "@neodrag/solid";
+import type { GridCell } from "../types/grid";
+import type { WidgetConfig } from "../types/widget";
 
 type DragEventData = {
   offsetX: number;
@@ -23,10 +25,6 @@ interface WidgetRendererProps {
   locked?: boolean;
 }
 
-// ============================================================================
-// SNAP SYSTEM - Reimplemented from scratch
-// ============================================================================
-
 interface Point {
   x: number;
   translate?: string;
@@ -47,24 +45,16 @@ interface SnapResult {
   size?: { width: number; height: number };
 }
 
-/**
- * Configuration for the snap system
- */
 const SNAP_CONFIG = {
-  // Distance threshold for magnetic snapping (in grid pixels)
-  // Large threshold makes snapping more forgiving
   threshold: 500,
-  // Minimum overlap ratio required to consider snapping (0-1)
+
   minOverlap: 0.15,
-  // Whether to snap during drag (magnetic) or only on drop
+
   magneticSnap: true,
-  // Whether to resize widget to fit cell when snapping
+
   resizeOnSnap: true,
 };
 
-/**
- * Calculate the center point of a rectangle
- */
 function getRectCenter(rect: Rect): Point {
   return {
     x: rect.x + rect.width / 2,
@@ -72,30 +62,12 @@ function getRectCenter(rect: Rect): Point {
   };
 }
 
-/**
- * Calculate distance between two points
- */
 function getDistance(p1: Point, p2: Point): number {
   const dx = p2.x - p1.x;
   const dy = p2.y - p1.y;
   return Math.sqrt(dx * dx + dy * dy);
 }
 
-/**
- * Check if a point is inside a rectangle
- */
-function isPointInRect(point: Point, rect: Rect): boolean {
-  return (
-    point.x >= rect.x &&
-    point.x <= rect.x + rect.width &&
-    point.y >= rect.y &&
-    point.y <= rect.y + rect.height
-  );
-}
-
-/**
- * Calculate overlap percentage between two rectangles (0-1)
- */
 function getOverlapRatio(rect1: Rect, rect2: Rect): number {
   const xOverlap = Math.max(
     0,
@@ -114,10 +86,6 @@ function getOverlapRatio(rect1: Rect, rect2: Rect): number {
   return rect1Area > 0 ? overlapArea / rect1Area : 0;
 }
 
-/**
- * Find the best cell to snap to based on widget position
- * Strategy: Use center point proximity with overlap as tiebreaker
- */
 function findSnapTarget(
   widgetRect: Rect,
   cells: GridCell[],
@@ -141,10 +109,8 @@ function findSnapTarget(
     const cellCenter = getRectCenter(cell);
     const distance = getDistance(widgetCenter, cellCenter);
 
-    // Only consider cells within threshold distance
     if (distance > threshold) continue;
 
-    // Calculate overlap - this is now the PRIMARY metric
     const overlap = getOverlapRatio(widgetRect, cell);
 
     console.log("[Snap Debug] Checking cell:", {
@@ -156,11 +122,8 @@ function findSnapTarget(
       meetsMinOverlap: overlap >= SNAP_CONFIG.minOverlap,
     });
 
-    // Only snap if there's meaningful overlap
     if (overlap < SNAP_CONFIG.minOverlap) continue;
 
-    // Choose cell with highest overlap (most intuitive for users)
-    // If overlap is equal, prefer the one with closer center
     if (
       overlap > bestOverlap ||
       (overlap === bestOverlap &&
@@ -186,9 +149,6 @@ function findSnapTarget(
   return bestCell;
 }
 
-/**
- * Calculate snap result for a widget at given position
- */
 function calculateSnap(
   widgetPosition: Point,
   widgetSize: { width: number; height: number },
@@ -230,10 +190,6 @@ function calculateSnap(
   };
 }
 
-// ============================================================================
-// WIDGET RENDERER COMPONENT
-// ============================================================================
-
 export function WidgetRenderer(props: WidgetRendererProps) {
   const widgetDef = getWidget(props.config.type);
   const { draggable } = createDraggable();
@@ -249,7 +205,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
   };
   let resizeStartPos: { x: number; y: number } = { x: 0, y: 0 };
 
-  // Track drag start position for offset calculation
   let dragStartWidgetPos: Point | null = null;
   let dragStartPointerPos: Point | null = null;
 
@@ -260,16 +215,12 @@ export function WidgetRenderer(props: WidgetRendererProps) {
     parentEl = document.querySelector("[data-grid-container]") as HTMLElement;
   });
 
-  /**
-   * Parse CSS transform scale from element
-   */
   const getContainerScale = (): number => {
     if (!parentEl) return 1;
     const transform = window.getComputedStyle(parentEl).transform;
     if (!transform || transform === "none") return 1;
 
     try {
-      // Parse matrix(a, b, c, d, tx, ty) - scale is 'a'
       const matrixMatch = transform.match(/matrix\(([^)]+)\)/);
       if (matrixMatch) {
         const values = matrixMatch[1]
@@ -278,7 +229,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
         return values[0] || 1;
       }
 
-      // Parse matrix3d - scale is first value
       const matrix3dMatch = transform.match(/matrix3d\(([^)]+)\)/);
       if (matrix3dMatch) {
         const values = matrix3dMatch[1]
@@ -292,9 +242,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
     return 1;
   };
 
-  /**
-   * Convert screen coordinates to grid coordinates
-   */
   const screenToGrid = (screenX: number, screenY: number): Point | null => {
     if (!parentEl) return null;
 
@@ -307,9 +254,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
     };
   };
 
-  /**
-   * Get current widget position in grid coordinates from DOM
-   */
   const getWidgetGridPosition = (): Point => {
     if (!widgetEl || !parentEl) return position();
 
@@ -329,7 +273,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
 
     if (!data || !data.event) return;
 
-    // Extract clientX/clientY from the event
     const event = data.event;
     const clientX =
       "clientX" in event
@@ -364,7 +307,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
 
     if (!data.event) return;
 
-    // Extract clientX/clientY from the event
     const event = data.event;
     const clientX =
       "clientX" in event
@@ -375,7 +317,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
         ? event.clientY
         : (event as TouchEvent).touches[0].clientY;
 
-    // Calculate current widget position
     const pointerPos = screenToGrid(clientX, clientY);
     if (!pointerPos || !dragStartPointerPos || !dragStartWidgetPos) return;
 
@@ -408,7 +349,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
       },
     });
 
-    // Update hover state
     props.onCellHover?.(snapResult.targetCell?.id || null);
   };
 
@@ -418,13 +358,11 @@ export function WidgetRenderer(props: WidgetRendererProps) {
     props.onCellHover?.(null);
 
     if (!data.event) {
-      // Fallback: no coordinate info
       dragStartWidgetPos = null;
       dragStartPointerPos = null;
       return;
     }
 
-    // Extract clientX/clientY from the event
     const event = data.event;
     const clientX =
       "clientX" in event
@@ -442,7 +380,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
       return;
     }
 
-    // Calculate final position
     const offset = {
       x: pointerPos.x - dragStartPointerPos.x,
       y: pointerPos.y - dragStartPointerPos.y,
@@ -453,7 +390,6 @@ export function WidgetRenderer(props: WidgetRendererProps) {
       y: dragStartWidgetPos.y + offset.y,
     };
 
-    // Attempt to snap
     const snapResult = calculateSnap(
       finalPos,
       size(),
@@ -461,13 +397,11 @@ export function WidgetRenderer(props: WidgetRendererProps) {
       true,
     );
 
-    // Apply position update
     props.onPositionUpdate?.(props.config.id, {
       x: Math.round(snapResult.position.x),
       y: Math.round(snapResult.position.y),
     });
 
-    // Apply size update if snapped
     if (snapResult.snapped && snapResult.size) {
       props.onSizeUpdate?.(props.config.id, {
         width: Math.round(snapResult.size.width),
@@ -475,10 +409,8 @@ export function WidgetRenderer(props: WidgetRendererProps) {
       });
     }
 
-    // Notify parent about snap state
     props.onSnapToCell?.(props.config.id, snapResult.targetCell?.id || null);
 
-    // Reset drag tracking
     dragStartWidgetPos = null;
     dragStartPointerPos = null;
   };

+ 0 - 3
src/grids/CenteredFocusGrid.tsx

@@ -8,15 +8,12 @@ export const centeredFocusGridTemplate: GridTemplate = {
   height: 760,
   background: "#1a1a1a",
   cells: [
-    // Left sidebar - 2 stacked boxes
     { id: "left-top", x: 10, y: 10, width: 390, height: 340 },
     { id: "left-bottom", x: 10, y: 360, width: 390, height: 390 },
 
-    // Center - 2 items (short wide on top, larger below)
     { id: "center-top", x: 410, y: 10, width: 760, height: 250 },
     { id: "center-bottom", x: 410, y: 270, width: 760, height: 480 },
 
-    // Right sidebar - single tall item
     { id: "right-tall", x: 1180, y: 10, width: 330, height: 740 },
   ],
 };

+ 0 - 3
src/grids/DashboardGrid.tsx

@@ -8,16 +8,13 @@ export const dashboardGridTemplate: GridTemplate = {
   height: 900,
   background: "#1a1a1a",
   cells: [
-    // Header row - full width
     { id: "header", x: 10, y: 10, width: 1380, height: 120 },
 
-    // Main metrics row
     { id: "metric-1", x: 10, y: 140, width: 330, height: 200 },
     { id: "metric-2", x: 350, y: 140, width: 330, height: 200 },
     { id: "metric-3", x: 690, y: 140, width: 330, height: 200 },
     { id: "metric-4", x: 1030, y: 140, width: 360, height: 200 },
 
-    // Middle row - large chart on left, two stacked on right
     { id: "chart-main", x: 10, y: 350, width: 880, height: 540 },
     { id: "info-top", x: 900, y: 350, width: 490, height: 260 },
     { id: "info-bottom", x: 900, y: 620, width: 490, height: 270 },

+ 0 - 6
src/grids/SidebarContentGrid.tsx

@@ -8,16 +8,10 @@ export const sidebarContentGridTemplate: GridTemplate = {
   height: 900,
   background: "#1a1a1a",
   cells: [
-    // Left sidebar - single long bar
     { id: "left-sidebar", x: 10, y: 10, width: 280, height: 880 },
 
-    // Main content - centered between sidebars
-    // Math: left ends at 290, right starts at 1110
-    // Available: 1110 - 290 = 820px
-    // Center: 290 + (820 - 800)/2 = 300
     { id: "main-content", x: 300, y: 10, width: 800, height: 880 },
 
-    // Right sidebar - single long bar
     { id: "right-sidebar", x: 1110, y: 10, width: 280, height: 880 },
   ],
 };

+ 0 - 3
src/grids/ThreeColumnGrid.tsx

@@ -8,15 +8,12 @@ export const threeColumnGridTemplate: GridTemplate = {
   height: 800,
   background: "#1a1a1a",
   cells: [
-    // Left sidebar - narrow
     { id: "sidebar-left", x: 10, y: 10, width: 200, height: 780 },
 
-    // Main content area - wide
     { id: "main-top", x: 220, y: 10, width: 760, height: 380 },
     { id: "main-bottom-left", x: 220, y: 400, width: 370, height: 390 },
     { id: "main-bottom-right", x: 600, y: 400, width: 380, height: 390 },
 
-    // Right panel - medium
     { id: "panel-top", x: 990, y: 10, width: 200, height: 380 },
     { id: "panel-bottom", x: 990, y: 400, width: 200, height: 390 },
   ],

+ 0 - 1
src/lib/prisma.ts

@@ -1,6 +1,5 @@
 import { PrismaClient } from "@prisma/client";
 
-// Prevent multiple instances of Prisma Client in development
 const globalForPrisma = globalThis as unknown as {
   prisma: PrismaClient | undefined;
 };

+ 1 - 1
src/pages/api/weather.ts

@@ -69,7 +69,7 @@ export const GET: APIRoute = async ({ request }) => {
       status: 200,
       headers: {
         "Content-Type": "application/json",
-        "Cache-Control": "public, max-age=300", // Cache for 5 minutes
+        "Cache-Control": "public, max-age=300",
       },
     });
   } catch (error) {

+ 407 - 0
src/pages/demo.astro

@@ -0,0 +1,407 @@
+<canvas id="tablet-canvas"></canvas>
+<script>
+    // @ts-ignore
+    import * as THREE from "three";
+    // @ts-ignore
+    import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
+
+    // Three.js scene setup
+    const canvas = document.getElementById(
+        "tablet-canvas",
+    ) as HTMLCanvasElement;
+    const scene = new THREE.Scene();
+    const camera = new THREE.PerspectiveCamera(
+        75,
+        canvas.clientWidth / canvas.clientHeight,
+        0.1,
+        1000,
+    );
+    const renderer = new THREE.WebGLRenderer({
+        canvas,
+        alpha: true,
+        antialias: true,
+    });
+
+    renderer.setSize(canvas.clientWidth, canvas.clientHeight);
+    renderer.setClearColor(0x000000, 0);
+
+    // Create dashboard textures
+    const createDashboardTexture = (type: string) => {
+        const canvas = document.createElement("canvas");
+        canvas.width = 512;
+        canvas.height = 768;
+        const ctx = canvas.getContext("2d")!;
+
+        // Background
+        ctx.fillStyle = "#f5f5f5";
+        ctx.fillRect(0, 0, 512, 768);
+
+        // Draw different dashboard layouts
+        ctx.strokeStyle = "#000";
+        ctx.fillStyle = "#000";
+        ctx.lineWidth = 2;
+
+        if (type === "grid") {
+            // Detailed productivity dashboard
+            ctx.font = "bold 20px Arial";
+            ctx.fillText("My Dashboard", 30, 40);
+            ctx.font = "12px Arial";
+            ctx.fillStyle = "#666";
+            ctx.fillText("Monday, Oct 14 • 12:34 PM", 30, 60);
+
+            // TODO Widget
+            ctx.fillStyle = "#000";
+            ctx.strokeRect(30, 80, 220, 180);
+            ctx.font = "bold 14px Arial";
+            ctx.fillText("Today's Tasks", 40, 105);
+
+            const todos = [
+                { text: "Review PR #234", done: true },
+                { text: "Deploy to prod", done: true },
+                { text: "Team standup 2pm", done: false },
+                { text: "Finish docs", done: false },
+            ];
+
+            todos.forEach((todo, i) => {
+                const y = 130 + i * 30;
+                // Checkbox
+                ctx.strokeRect(45, y - 12, 14, 14);
+                if (todo.done) {
+                    ctx.fillStyle = "#000";
+                    ctx.fillRect(48, y - 9, 8, 8);
+                }
+                // Text
+                ctx.font = "12px Arial";
+                ctx.fillStyle = todo.done ? "#999" : "#000";
+                ctx.fillText(todo.text, 68, y);
+            });
+
+            // Monero Price Widget
+            ctx.fillStyle = "#000";
+            ctx.strokeRect(270, 80, 210, 180);
+            ctx.font = "bold 14px Arial";
+            ctx.fillText("Monero (XMR)", 280, 105);
+            ctx.font = "bold 32px Arial";
+            ctx.fillText("$168.42", 280, 150);
+            ctx.font = "14px Arial";
+            ctx.fillStyle = "#2ecc71";
+            ctx.fillText("+5.2% (24h)", 280, 175);
+
+            // Mini chart
+            ctx.strokeStyle = "#2ecc71";
+            ctx.lineWidth = 2;
+            ctx.beginPath();
+            const chartPoints = [0, -5, 3, -2, 8, 12, 15, 10, 18, 22, 20];
+            chartPoints.forEach((val, i) => {
+                const x = 280 + i * 18;
+                const y = 220 - val;
+                if (i === 0) ctx.moveTo(x, y);
+                else ctx.lineTo(x, y);
+            });
+            ctx.stroke();
+            ctx.lineWidth = 2;
+            ctx.strokeStyle = "#000";
+
+            // Notion Assignments Widget
+            ctx.fillStyle = "#000";
+            ctx.strokeRect(30, 280, 220, 160);
+            ctx.font = "bold 14px Arial";
+            ctx.fillText("Notion - Assignments", 40, 305);
+
+            const assignments = [
+                { title: "Q4 Roadmap", due: "Oct 18" },
+                { title: "Design Review", due: "Oct 20" },
+                { title: "API Spec Doc", due: "Oct 25" },
+            ];
+
+            assignments.forEach((item, i) => {
+                const y = 335 + i * 35;
+                ctx.font = "12px Arial";
+                ctx.fillStyle = "#000";
+                ctx.fillText("•", 45, y);
+                ctx.fillText(item.title, 60, y);
+                ctx.font = "10px Arial";
+                ctx.fillStyle = "#999";
+                ctx.fillText("Due " + item.due, 60, y + 15);
+            });
+
+            // Custom HTML Widget
+            ctx.fillStyle = "#000";
+            ctx.strokeRect(270, 280, 210, 160);
+            ctx.font = "bold 14px Arial";
+            ctx.fillText("</> Custom HTML", 280, 305);
+
+            // Mock HTML code
+            ctx.font = "10px monospace";
+            ctx.fillStyle = "#666";
+            ctx.fillText('<div class="widget">', 280, 330);
+            ctx.fillText("  <h2>Hello</h2>", 280, 345);
+            ctx.fillText("  <p>Custom</p>", 280, 360);
+            ctx.fillText("  render()", 280, 375);
+            ctx.fillText("</div>", 280, 390);
+
+            // Quick Stats Widget
+            ctx.fillStyle = "#000";
+            ctx.strokeRect(30, 460, 450, 100);
+            ctx.font = "bold 14px Arial";
+            ctx.fillText("Quick Stats", 40, 485);
+
+            // Three stat boxes
+            const stats = [
+                { label: "Tasks Done", value: "12/16" },
+                { label: "Focus Time", value: "3.5h" },
+                { label: "Streak", value: "7 days" },
+            ];
+
+            stats.forEach((stat, i) => {
+                const x = 50 + i * 145;
+                ctx.font = "bold 24px Arial";
+                ctx.fillText(stat.value, x, 520);
+                ctx.font = "11px Arial";
+                ctx.fillStyle = "#888";
+                ctx.fillText(stat.label, x, 540);
+                ctx.fillStyle = "#000";
+            });
+        } else if (type === "weather") {
+            // Weather-focused layout
+            ctx.font = "bold 80px Arial";
+            ctx.fillText("72°", 40, 180);
+            ctx.font = "24px Arial";
+            ctx.fillText("Partly Cloudy", 40, 220);
+
+            // Draw simple sun icon
+            ctx.beginPath();
+            ctx.arc(400, 130, 40, 0, Math.PI * 2);
+            ctx.stroke();
+
+            // Week forecast
+            const days = ["MON", "TUE", "WED", "THU", "FRI"];
+            days.forEach((day, i) => {
+                const x = 40 + i * 90;
+                ctx.font = "12px Arial";
+                ctx.fillText(day, x, 320);
+                ctx.font = "20px Arial";
+                ctx.fillText("7" + i, x, 360);
+            });
+
+            // Time widget
+            ctx.strokeRect(40, 420, 430, 120);
+            ctx.font = "bold 60px Arial";
+            ctx.fillStyle = "#000";
+            ctx.fillText("2:34 PM", 60, 490);
+            ctx.font = "18px Arial";
+            ctx.fillStyle = "#666";
+            ctx.fillText("Monday, October 14", 60, 520);
+        } else if (type === "crypto") {
+            // Crypto dashboard
+            ctx.font = "bold 20px Arial";
+            ctx.fillText("Portfolio", 40, 60);
+
+            const cryptos = [
+                { name: "BTC", price: "$64,234", change: "+2.4%" },
+                { name: "ETH", price: "$3,421", change: "+1.8%" },
+                { name: "SOL", price: "$142", change: "-0.5%" },
+            ];
+
+            cryptos.forEach((crypto, i) => {
+                const y = 120 + i * 100;
+                ctx.font = "bold 18px Arial";
+                ctx.fillText(crypto.name, 40, y);
+                ctx.font = "24px Arial";
+                ctx.fillText(crypto.price, 40, y + 35);
+                ctx.font = "16px Arial";
+                ctx.fillText(crypto.change, 40, y + 60);
+
+                // Simple chart line
+                ctx.beginPath();
+                ctx.moveTo(250, y + 20);
+                for (let j = 0; j < 10; j++) {
+                    ctx.lineTo(250 + j * 22, y + 20 + Math.random() * 40 - 20);
+                }
+                ctx.stroke();
+            });
+
+            // Stoic quote widget
+            ctx.fillStyle = "#000";
+            ctx.strokeRect(40, 450, 430, 110);
+            ctx.font = "italic 20px Arial";
+            ctx.fillStyle = "#222";
+            ctx.fillText('"You have power over', 60, 485);
+            ctx.fillText('your mind, not outside events."', 60, 515);
+            ctx.font = "16px Arial";
+            ctx.fillStyle = "#666";
+            ctx.fillText("— Marcus Aurelius", 60, 545);
+        }
+
+        return new THREE.CanvasTexture(canvas);
+    };
+
+    // Create tablet frame (improved 3-part design)
+    const createTablet = () => {
+        const group = new THREE.Group();
+
+        // Create dashboard textures
+        const dashboardTextures = [
+            createDashboardTexture("grid"),
+            createDashboardTexture("weather"),
+            createDashboardTexture("crypto"),
+        ];
+
+        // Main screen (e-ink display) with dashboard texture
+        const screenGeometry = new THREE.BoxGeometry(4, 6, 0.06);
+        const screenMaterial = new THREE.MeshStandardMaterial({
+            map: dashboardTextures[0],
+            roughness: 0.9,
+            metalness: 0.05,
+        });
+        const screen = new THREE.Mesh(screenGeometry, screenMaterial);
+        screen.userData.textures = dashboardTextures;
+        screen.userData.currentTexture = 0;
+        screen.position.z = 0.04;
+        group.add(screen);
+
+        // Frame parts (3-part modular system with better detail)
+        const frameMaterial = new THREE.MeshStandardMaterial({
+            color: 0x0a0a0a,
+            roughness: 0.6,
+            metalness: 0.3,
+        });
+
+        // Top frame part - flush with screen
+        const topFrame = new THREE.Mesh(
+            new THREE.BoxGeometry(4.2, 0.2, 0.15),
+            frameMaterial,
+        );
+        topFrame.position.y = 3.1;
+        topFrame.position.z = 0.04;
+        group.add(topFrame);
+
+        // Side frames (left and right) - flush with screen
+        const sideFrame = new THREE.Mesh(
+            new THREE.BoxGeometry(0.1, 6, 0.15),
+            frameMaterial,
+        );
+        sideFrame.position.x = -2.05;
+        sideFrame.position.z = 0.04;
+        group.add(sideFrame);
+
+        const rightSideFrame = sideFrame.clone();
+        rightSideFrame.position.x = 2.05;
+        group.add(rightSideFrame);
+
+        // Bottom frame part - flush with screen
+        const bottomFrame = new THREE.Mesh(
+            new THREE.BoxGeometry(4.2, 0.2, 0.15),
+            frameMaterial,
+        );
+        bottomFrame.position.y = -3.1;
+        bottomFrame.position.z = 0.04;
+        group.add(bottomFrame);
+
+        // Back plate with mounting points
+        const backPlate = new THREE.Mesh(
+            new THREE.BoxGeometry(4.5, 6.5, 0.12),
+            frameMaterial,
+        );
+        backPlate.position.z = -0.18;
+        group.add(backPlate);
+
+        // Add modular connection indicators (small dots)
+        const connectionMaterial = new THREE.MeshStandardMaterial({
+            color: 0x333333,
+            roughness: 0.5,
+            metalness: 0.5,
+        });
+
+        for (let i = 0; i < 3; i++) {
+            const dot = new THREE.Mesh(
+                new THREE.SphereGeometry(0.04, 8, 8),
+                connectionMaterial,
+            );
+            dot.position.set(-2.13, 2 - i * 2, 0.17);
+            group.add(dot);
+
+            const dotRight = dot.clone();
+            dotRight.position.x = 2.13;
+            group.add(dotRight);
+        }
+
+        return group;
+    };
+
+    const tablet = createTablet();
+    scene.add(tablet);
+
+    // Get screen for texture animation
+    const screen = tablet.children.find(
+        (child: any) => child.userData.textures,
+    ) as THREE.Mesh;
+    let lastTextureChange = Date.now();
+    const textureChangeInterval = 4000; // 4 seconds
+
+    // Lighting
+    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
+    scene.add(ambientLight);
+
+    const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
+    directionalLight.position.set(5, 5, 5);
+    scene.add(directionalLight);
+
+    const rimLight = new THREE.DirectionalLight(0xff6b6b, 0.3);
+    rimLight.position.set(-5, 0, -5);
+    scene.add(rimLight);
+
+    camera.position.set(0, 0, 6);
+
+    // Add OrbitControls for user interaction
+    const controls = new OrbitControls(camera, canvas);
+    controls.enableDamping = true;
+    controls.dampingFactor = 0.05;
+    controls.enableZoom = true;
+    controls.enablePan = false;
+    controls.minDistance = 4;
+    controls.maxDistance = 12;
+    controls.autoRotate = true;
+    controls.autoRotateSpeed = 0.5;
+
+    const animate = () => {
+        requestAnimationFrame(animate);
+
+        // Update controls
+        controls.update();
+
+        // Handle dashboard texture transitions
+        const now = Date.now();
+        if (now - lastTextureChange > textureChangeInterval) {
+            const material = screen.material as THREE.MeshStandardMaterial;
+            const nextTexture =
+                (screen.userData.currentTexture + 1) %
+                screen.userData.textures.length;
+
+            // Fade out/in effect via opacity
+            const fadeProgress = Math.min(
+                (now - lastTextureChange - textureChangeInterval) / 500,
+                1,
+            );
+
+            if (fadeProgress >= 1) {
+                // Switch texture
+                material.map = screen.userData.textures[nextTexture];
+                material.needsUpdate = true;
+                screen.userData.currentTexture = nextTexture;
+                lastTextureChange = now;
+            }
+        }
+
+        renderer.render(scene, camera);
+    };
+
+    animate();
+
+    // Handle window resize
+    window.addEventListener("resize", () => {
+        camera.aspect = canvas.clientWidth / canvas.clientHeight;
+        camera.updateProjectionMatrix();
+        renderer.setSize(canvas.clientWidth, canvas.clientHeight);
+    });
+</script>

+ 9 - 0
wrangler.jsonc

@@ -0,0 +1,9 @@
+{
+  "name": "glance",
+  "compatibility_date": "2025-10-23", // Update to the day you deploy
+  "compatibility_flags": [
+    "nodejs_compat",
+    "disable_nodejs_process_v2"
+  ],
+  "pages_build_output_dir": "./dist"
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff