npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

canvasframework

v0.4.11

Published

Canvas-based cross-platform UI framework (Material & Cupertino)

Readme

Canvas UI Engine (UI engine inspired by Flutter, built for the Web)

Canvas-based UI Engine for Mobile & Embedded Apps
A high-performance UI engine rendered with Canvas/WebGL, running inside a WebView runtime (Capacitor / Cordova), without DOM, HTML or CSS.


Installation

  • npm install canvasframework
  • npm install -D vite

add in package.json

"scripts": { "dev": "vite", "build": "vite build" }

launch

npm run dev

🚀 Overview

Canvas UI Engine is a low-level UI engine that renders the entire user interface using Canvas 2D or WebGL, instead of the DOM.

Although it runs inside a WebView, it does not rely on HTML, CSS, or the browser layout engine.
The WebView is used only as a JavaScript runtime, not as a UI system.

Think Flutter’s rendering model, but implemented in JavaScript, running inside a WebView.


❌ What this engine is NOT

  • ❌ Not a web framework
  • ❌ Not DOM-based
  • ❌ Not Ionic / React / Vue
  • ❌ Not HTML/CSS driven
  • ❌ Not designed for SEO or websites

✅ What this engine IS

  • ✅ A Canvas-first UI engine
  • ✅ A custom rendering pipeline
  • ✅ A custom layout system
  • ✅ A custom input & gesture system
  • ✅ A deterministic, app-like UI
  • ✅ Designed for mobile & embedded apps

🧠 Architecture

The WebView is only a runtime shell.
The UI is fully controlled by the engine (no DOM, no HTML layout).

Key point

The browser does not manage UI.
The engine does.


🎨 Rendering

  • Canvas 2D (CPU)
  • Optional WebGL backend (GPU accelerated)
  • Device Pixel Ratio aware
  • Dirty-region rendering
  • Scene graph based

Rendering performance depends entirely on the engine, not on the browser DOM.


🧩 UI Model

  • Every component is drawn, not mounted
  • No HTML nodes
  • No CSS
  • No reflow
  • No repaint cost from the browser

Example components:

  • Button
  • Text
  • Input
  • Slider
  • Switch
  • List / Card
  • Modal / Dialog / BottomSheet
  • Navigation & routing system

🧭 Navigation

  • Internal routing system
  • Stack-based navigation
  • Animated transitions (slide / fade)
  • Independent from browser routing logic

🧵 Multithreading

  • UI Worker → layout & scroll inertia
  • Logic Worker → business logic
  • Main thread → rendering only

This allows smoother UI and better separation of concerns.


🔌 Native capabilities

Native features are accessed via the host runtime (Capacitor / Cordova):

  • Camera
  • Filesystem
  • Secure storage
  • Geolocation
  • WebSocket
  • Network

⚠️ Plugins that require DOM access are not supported.


📦 Data & Networking

The engine provides its own service layer:

  • Cached fetch service
  • WebSocket manager
  • Geolocation abstraction
  • Offline-first data handling

All APIs are engine-controlled, not browser-controlled.


🆚 Comparison

vs React Native

| React Native | Canvas UI Engine | |-------------|-----------------| | Native views | Custom rendering | | Bridge overhead | Direct rendering | | Platform UI | Engine UI | | DOM-like model | Scene graph |


vs Flutter

| Flutter | Canvas UI Engine | |--------|------------------| | Native engine | WebView runtime | | Skia | Canvas / WebGL | | Dart | JavaScript | | Compiled | Interpreted | | UI engine | UI engine |

👉 Same architecture philosophy, different runtime constraints.


⚠️ Known limitations

  • No DOM access
  • No HTML rendering
  • No SEO
  • No accessibility (yet)
  • Text shaping is basic (LTR focused)

These are intentional design decisions.


🎯 Target use cases

  • Mobile applications
  • Embedded systems
  • Kiosk interfaces
  • Medical / industrial apps
  • Offline-first apps
  • UI where control > compatibility

🛣️ Roadmap (simplified)

Phase 1 – Core engine (current)

  • Canvas/WebGL rendering
  • Input & layout
  • Core components

Phase 2 – Performance & UX

  • Gesture system
  • Animation engine
  • Advanced scrolling
  • Asset caching

Phase 3 – Ecosystem

  • Plugin bridge
  • Devtools
  • Theming system

🧪 Philosophy

The WebView is just a runtime.
The UI is owned by the engine.

If the engine is fast → the app is fast.
If the engine is slow → nothing can save it.


Exemples

Exemple – Accordion

Afficher des sections d’informations extensibles (FAQ, paramètres, détails).

import { Accordion } from './framework/index.js';

const accordion = new Accordion(app, {
  x: 16,
  y: 80,
  width: app.width - 32,
  title: 'Informations du compte',
  icon: 'ℹ️',
  content: 'Votre compte vous permet de gérer vos préférences, votre sécurité et vos informations personnelles.',
  expanded: false,
  onToggle: (expanded) => {
    console.log('Accordion ouvert ?', expanded);
  }
});

app.add(accordion);

Button (Material / Cupertino auto)

Action principale (submit, navigation, confirmation).

import { Button } from './framework/index.js';

const button = new Button(app, {
  x: 16,
  y: 300,
  width: app.width - 32,
  height: 48,
  text: 'Continuer',
  elevation: 4
});

button.onClick = () => {
  console.log('Bouton cliqué');
};

app.add(button);

Exemple – BottomNavigationBar

Navigation principale d’application mobile.

import { BottomNavigationBar } from './framework/index.js';

const bottomNav = new BottomNavigationBar(app, {
  items: [
    { icon: 'home', label: 'Accueil' },
    { icon: 'search', label: 'Recherche' },
    { icon: 'favorite', label: 'Favoris' },
    { icon: 'person', label: 'Profil' }
  ],
  onChange: (index, item) => {
    console.log('Onglet sélectionné:', index, item.label);
  }
});

app.add(bottomNav);

Exemple – Dialog (alert / confirmation)

Alerte, confirmation, choix utilisateur.

import { Dialog, Button } from './framework/index.js';

const showDialogBtn = new Button(app, {
  x: 16,
  y: 380,
  width: app.width - 32,
  height: 48,
  text: 'Supprimer le compte'
});

showDialogBtn.onClick = () => {
  const dialog = new Dialog(app, {
    title: 'Confirmation',
    message: 'Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.',
    buttons: ['Annuler', 'Supprimer'],
    onButtonClick: (index, label) => {
      console.log('Bouton dialog:', label);
    }
  });

  app.add(dialog);
  dialog.show();
};

app.add(showDialogBtn);

Exemple – Combinaison réelle (mini écran)

app.add(new Text(app, {
  x: 16,
  y: 24,
  text: 'Paramètres',
  fontSize: 22,
  bold: true
}));

// Accordéon
app.add(new Accordion(app, {
  x: 16,
  y: 80,
  width: app.width - 32,
  title: 'Sécurité',
  content: 'Changez votre mot de passe, activez la double authentification.'
}));

// Bouton
app.add(new Button(app, {
  x: 16,
  y: 240,
  width: app.width - 32,
  height: 48,
  text: 'Déconnexion'
}));

// Navigation
app.add(new BottomNavigationBar(app, {
  items: [
    { icon: 'home', label: 'Accueil' },
    { icon: 'settings', label: 'Paramètres' }
  ]
}));

Stack – superposition (équivalent Flutter Stack)

Exemple : carte avec image + titre + bouton flottant

import Stack from './layout/Stack.js';
import Text from './components/Text.js';
import Button from './components/Button.js';
import Image from './components/Image.js';

const card = new Stack(app, {
  x: 16,
  y: 40,
  width: app.width - 32,
  height: 200
});

card.add(new Image(app, {
  src: 'cover.jpg',
  width: card.width,
  height: 200
}));

card.add(new Text(app, {
  x: 16,
  y: 16,
  text: 'Titre de la carte',
  fontSize: 20,
  color: '#FFFFFF'
}));

card.add(new Button(app, {
  x: card.width - 72,
  y: card.height - 56,
  width: 56,
  height: 56,
  text: '+'
}));

app.add(card);
card.layoutRecursive();

Column – layout vertical (Flutter Column)

Exemple : écran de paramètres

import Column from './layout/Column.js';
import Text from './components/Text.js';
import Button from './components/Button.js';

const column = new Column(app, {
  x: 16,
  y: 40,
  width: app.width - 32,
  spacing: 12,
  align: 'stretch'
});

column.add(new Text(app, {
  text: 'Paramètres',
  fontSize: 22,
  height: 32
}));

column.add(new Button(app, {
  height: 48,
  text: 'Compte'
}));

column.add(new Button(app, {
  height: 48,
  text: 'Sécurité'
}));

column.add(new Button(app, {
  height: 48,
  text: 'Notifications'
}));

app.add(column);
column.layout();

Row – layout horizontal (Flutter Row)

Exemple : barre d’actions

import Row from './layout/Row.js';
import Button from './components/Button.js';

const actions = new Row(app, {
  x: 16,
  y: 120,
  height: 48,
  spacing: 12,
  align: 'center'
});

actions.add(new Button(app, {
  width: 100,
  height: 40,
  text: 'Annuler'
}));

actions.add(new Button(app, {
  width: 120,
  height: 40,
  text: 'Valider'
}));

app.add(actions);
actions.layout();

Grid – grille adaptative (Flutter GridView.count)

Exemple : grille d’options

import Grid from './layout/Grid.js';
import Card from './components/Card.js';

const grid = new Grid(app, {
  x: 16,
  y: 200,
  width: app.width - 32,
  columns: 3,
  spacing: 12
});

for (let i = 0; i < 6; i++) {
  grid.add(new Card(app, {
    height: 100,
    title: `Item ${i + 1}`
  }));
}

app.add(grid);
grid.layout();

Composition réelle (comme Flutter 😏)

const screen = new Column(app, {
  x: 0,
  y: 0,
  width: app.width,
  spacing: 24,
  align: 'stretch'
});

// Header
screen.add(new Text(app, {
  text: 'Accueil',
  fontSize: 24,
  height: 40
}));

// Hero
const hero = new Stack(app, {
  width: app.width,
  height: 180
});

hero.add(new Image(app, {
  src: 'hero.jpg',
  width: app.width,
  height: 180
}));

hero.add(new Text(app, {
  x: 16,
  y: 120,
  text: 'Bienvenue',
  fontSize: 20,
  color: '#FFF'
}));

screen.add(hero);

// Actions
const row = new Row(app, {
  height: 48,
  spacing: 12
});

row.add(new Button(app, { width: 120, height: 40, text: 'Explorer' }));
row.add(new Button(app, { width: 120, height: 40, text: 'Profil' }));

screen.add(row);

app.add(screen);
screen.layoutRecursive();

UIBuilder

with UIBuilder you can build your interface with more simplicity

import { CanvasFramework, Column, ui, createRef, Table, Divider, MorphingFAB, PasswordInput, InputTags, InputDatalist, SpeedDialFAB, FAB, FileUpload, OpenStreetMap, SignaturePad, TreeView, SearchInput, ContextMenu, BottomNavigationBar, Card, View, RadioButton, Dialog, Checkbox, PullToRefresh, ProgressBar, AppBar, Skeleton, Drawer, Text, Button, Input, Slider, Select, Switch } from './canvas-framework/index.js';

const app = new CanvasFramework('app-canvas',{ 
  useWebGL: true, 
  showFps: true,
  debug: true,
});

app.useWebGL = true;

// Route principale
app.route('/', (framework) => {
  const platform = framework.platform === 'material' ? 'Material Design' : 'Cupertino (iOS)';
  
  if (framework.useWebGL) {
    console.log("✅ WebGL est activé");
  } else {
    console.log("⚠️  WebGL non disponible, fallback en Canvas 2D");
  }

  // ✅ Créer une ref pour le texte du slider
  const sliderValueTextRef = createRef();

  ui.app(
    ui.Column({ x: 0, y: 0, spacing: 0 }, [
      // Titre
      ui.Text({
        x: 20,
        y: 50,
        width: framework.width - 40,
        text: `Canvas Framework - ${platform}`,
        fontSize: 24,
        bold: true,
        align: 'center'
      }),

      // Bouton Toast
      ui.Button({
        x: framework.width / 2 - 100,
        y: 120,
        width: 200,
        height: 50,
        text: 'Afficher Toast',
        onClick: () => {
          framework.showToast('Toast affiché avec succès!', 2000);
        }
      }),

      // Input
      ui.Input({
        x: 20,
        y: 200,
        width: framework.width - 40,
        height: 50,
        placeholder: 'Entrez du texte...'
      }),

      // Label Slider avec ref
      ui.Text({
        ref: sliderValueTextRef, // ✅ Utiliser ref au lieu de onMount
        x: 20,
        y: 280,
        width: framework.width - 40,
        text: 'Valeur: 50',
        fontSize: 14,
        color: '#666666'
      }),

      // Slider
      ui.Slider({
        x: 20,
        y: 310,
        width: framework.width - 40,
        height: 40,
        value: 50,
        onChange: (value) => {
          if (sliderValueTextRef.current) {
            sliderValueTextRef.current.text = `Valeur: ${Math.round(value)}`;
            sliderValueTextRef.current.markDirty();
          }
        }
      }),

      // Label Select
      ui.Text({
        x: 20,
        y: 380,
        width: framework.width - 40,
        text: 'Menu déroulant:',
        fontSize: 14,
        color: '#666666'
      }),

      // Select
      ui.Select({
        x: 20,
        y: 410,
        width: framework.width - 40,
        height: 50,
        options: ['Option 1', 'Option 2', 'Option 3'],
        selectedIndex: 0,
        onChange: (selectedOption, selectedIndex) => {
          console.log(`Option sélectionnée : ${selectedOption} (index: ${selectedIndex})`);
        }
      }),

      // Label Switch
      ui.Text({
        x: 20,
        y: 490,
        width: framework.width - 100,
        text: 'Activer notifications',
        fontSize: 16
      }),

      // Switch
      ui.Switch({
        x: framework.width - 71,
        y: 485,
        checked: false,
        onChange: (checked) => {
          framework.showToast(checked ? 'Notifications activées' : 'Notifications désactivées');
        }
      }),

      // Bouton Page 2
      ui.Button({
        x: 20,
        y: 550,
        type: 'outlined',
        shape: 'square',
        width: framework.width / 2 - 30,
        height: 50,
        text: 'Page 2 →',
        bgColor: framework.platform === 'material' ? '#03DAC6' : '#FF9500',
        onClick: () => {
          framework.navigate('/page2', { transition: 'fade' });
        }
      }),

      // Bouton Tout Tester
      ui.Button({
        x: framework.width / 2 + 10,
        y: 550,
        width: framework.width / 2 - 30,
        height: 50,
        type: 'filled',
        shape: 'rounded',
        text: 'Tout Tester →',
        bgColor: framework.platform === 'material' ? '#FF9800' : '#34C759',
        onClick: () => {
          framework.navigate('/test', { transition: 'slide' });
        }
      })
    ])
  ).mount(framework);
});

// Page 2 avec Drawer et Navigation
app.route('/page2', (framework) => {
  // ✅ Créer une ref pour le drawer
  const drawerRef = createRef();
  
  ui.app(
    ui.Column({ x: 0, y: 0, spacing: 0 }, [
      
      // Drawer avec ref
      ui.Drawer({
        ref: drawerRef, // ✅ Utiliser ref
        header: { title: 'Mon App' },
        items: [
          { icon: '🏠', label: 'Accueil' },
          { icon: '⚙️', label: 'Paramètres' },
          { icon: '❤️', label: 'Favoris', divider: true },
          { icon: '👤', label: 'Profil' }
        ],
        onItemClick: (index, item) => {
          framework.showToast(`${item.label} cliqué`);
          framework.navigate('/', { transition: 'none' });
        }
      }),
      
      // AppBar (sera au-dessus)
      ui.AppBar({
        title: 'Accueil',
        leftIcon: 'menu',
        rightIcon: 'search',
        onLeftClick: () => {
          if (drawerRef.current) drawerRef.current.open();
        },
        onRightClick: () => {
          framework.showToast('Recherche');
        }
      }),
      
      // PasswordInput
      ui.PasswordInput({
        placeholder: 'Entrez votre mot de passe',
        value: '',
        fontSize: 16,
        x: 20,
        y: 120,
        width: 300,
        height: 40,
        maskChar: '•',
        showPassword: false,
        onFocus: () => {
          console.log('PasswordInput focus');
        },
        onBlur: () => {
          console.log('PasswordInput blur');
        }
      }),
      
      // InputTags
      ui.InputTags({
        placeholder: 'Ajouter des tags...',
        tags: ['javascript', 'canvas'],
        x: 20,
        y: 160,
        width: 300,
        height: 50,
        tagColor: '#E3F2FD',
        tagTextColor: '#1565C0',
        deleteButtonColor: '#1565C0',
        onTagAdd: (tag, allTags) => {
          console.log('Tag ajouté:', tag, 'Tags:', allTags);
        },
        onTagRemove: (tag, allTags) => {
          console.log('Tag supprimé:', tag, 'Tags:', allTags);
        }
      }),
      
      // InputDatalist
      ui.InputDatalist({
        placeholder: 'Sélectionnez un pays...',
        value: '',
        options: [
          'France', 'Allemagne', 'Espagne', 'Italie', 'Portugal',
          'Belgique', 'Suisse', 'Canada', 'États-Unis', 'Japon',
          'Chine', 'Corée du Sud', 'Australie', 'Brésil', 'Mexique'
        ],
        x: 50,
        y: 210,
        width: 300,
        height: 40,
        maxDropdownItems: 8,
        dropdownBackground: '#FFFFFF',
        hoverBackground: '#F0F0F0',
        selectedBackground: '#E3F2FD',
        borderColor: '#CCCCCC',
        onSelect: (selectedValue) => {
          console.log('Option sélectionnée:', selectedValue);
        },
        onInput: (currentValue) => {
          console.log('Valeur en cours:', currentValue);
        }
      }),
      
      // BottomNavigationBar
      ui.BottomNavigationBar({
        items: [
          { icon: 'home', label: 'Home' },
          { icon: 'search', label: 'Search' },
          { icon: 'favorite', label: 'Favorites' },
          { icon: 'person', label: 'Profile' },
          { icon: 'settings', label: 'Settings' }
        ],
        selectedIndex: 0,
        selectedColor: '#6200EE',
        onChange: (index, item) => {
          console.log(`Tab changed to: ${item.label} (index ${index})`);
          switch(index) {
            case 0: framework.navigate('home'); break;
            case 1: framework.navigate('search'); break;
            case 2: framework.navigate('favorites'); break;
            case 3: framework.navigate('profile'); break;
            case 4: framework.navigate('settings'); break;
          }
        }
      })
      
    ])
  ).mount(framework);
});

// Page de test complète
app.route('/test', (framework) => {
  // ✅ Créer toutes les refs nécessaires
  const progressBarRef = createRef();
  const testInputRef = createRef();
  const sliderDisplayRef = createRef();
  const testSwitchRef = createRef();
  
  let yPosition = 80;
  
  ui.app(
    ui.Column({ x: 0, y: 0, spacing: 0 }, [
      
      // AppBar de retour (fixe)
      ui.AppBar({
        title: 'Test Complet',
        leftIcon: 'back',
        onLeftClick: () => {
          framework.navigate('/', { transition: 'slide' });
        }
      }),
      
      // Titre principal
      ui.Text({
        x: 20,
        y: yPosition,
        width: framework.width - 40,
        text: 'Test de tous les composants',
        fontSize: 24,
        bold: true,
        align: 'center'
      }),
      
      // 1. ProgressBar Section
      ui.Text({
        x: 20,
        y: yPosition + 60,
        width: framework.width - 40,
        text: '1. ProgressBar:',
        fontSize: 18,
        bold: true
      }),
      
      // ✅ ProgressBar avec ref - PLUS BESOIN de création manuelle !
      ui.ProgressBar({
        ref: progressBarRef,
        x: 20,
        y: yPosition + 90,
        width: framework.width - 40,
        progress: 30
      }),
      
      ui.Button({
        x: 20,
        y: yPosition + 140,
        width: 150,
        height: 40,
        text: 'Augmenter',
        onClick: () => {
          if (progressBarRef.current) {
            progressBarRef.current.progress = Math.min(100, progressBarRef.current.progress + 10);
            progressBarRef.current.markDirty();
          }
        }
      }),
      
      // 2. RadioButton Section
      ui.Text({
        x: 20,
        y: yPosition + 200,
        width: framework.width - 40,
        text: '2. RadioButton (Groupe 1):',
        fontSize: 18,
        bold: true
      }),
      
      ui.RadioButton({
        x: 40,
        y: yPosition + 230,
        group: 'groupe1',
        label: 'Option A',
        checked: true,
        onChange: (checked) => {
          if (checked) framework.showToast('Radio A sélectionné');
        }
      }),
      
      ui.RadioButton({
        x: 40,
        y: yPosition + 270,
        group: 'groupe1',
        label: 'Option B',
        onChange: (checked) => {
          if (checked) framework.showToast('Radio B sélectionné');
        }
      }),
      
      // 3. Checkbox Section
      ui.Text({
        x: 20,
        y: yPosition + 330,
        width: framework.width - 40,
        text: '3. Checkbox:',
        fontSize: 18,
        bold: true
      }),
      
      ui.Checkbox({
        x: 40,
        y: yPosition + 360,
        label: 'Accepter les termes',
        checked: false,
        onChange: (checked) => {
          framework.showToast(checked ? 'Coché' : 'Décoché');
        }
      }),
      
      // 4. Card Section
      ui.Text({
        x: 20,
        y: yPosition + 420,
        width: framework.width - 40,
        text: '4. Card:',
        fontSize: 18,
        bold: true
      }),
      
      ui.Card({
        x: 20,
        y: yPosition + 450,
        width: framework.width - 40,
        height: 180,
        padding: 16,
        elevation: 4,
        borderRadius: 8
      }, [
        ui.Text({
          x: 0,
          y: 0,
          width: framework.width - 72,
          text: 'Titre de la carte',
          fontSize: 18,
          bold: true
        }),
        
        ui.Text({
          x: 0,
          y: 40,
          width: framework.width - 72,
          maxWidth: framework.width - 72,
          text: 'Ceci est un exemple de texte à l\'intérieur d\'une carte. Le texte ne devrait plus déborder maintenant car il va à la ligne automatiquement quand il atteint la largeur maximale.',
          fontSize: 14,
          color: '#666666',
          wrap: true
        }),
        
        ui.Button({
          x: 0,
          y: 120,
          width: 150,
          height: 40,
          text: 'Bouton dans Card',
          onClick: () => {
            framework.showToast('Bouton dans Card cliqué!');
          }
        })
      ]),
      
      // 5. Dialog Section
      ui.Text({
        x: 20,
        y: yPosition + 650,
        width: framework.width - 40,
        text: '5. Dialog:',
        fontSize: 18,
        bold: true
      }),
      
      ui.Button({
        x: 20,
        y: yPosition + 680,
        width: 200,
        height: 50,
        text: 'Afficher Dialog',
        onClick: () => {
          const dialog = new Dialog(framework, {
            title: 'Titre du Dialog',
            message: 'Ceci est un message de dialog. Voulez-vous continuer ?',
            buttons: ['Annuler', 'OK'],
            onButtonClick: (index, text) => {
              framework.showToast(`Bouton cliqué: ${text}`);
            }
          });
          framework.add(dialog);
          dialog.show();
        }
      }),
      
      // 6. View Section
      ui.Text({
        x: 20,
        y: yPosition + 760,
        width: framework.width - 40,
        text: '6. View (Container):',
        fontSize: 18,
        bold: true
      }),
      
      ui.View({
        x: 20,
        y: yPosition + 790,
        width: framework.width - 40,
        height: 200,
        padding: 20,
        gap: 10,
        direction: 'column',
        bgColor: '#F0F0F0',
        borderRadius: 8
      }, [
        ui.Text({
          width: framework.width - 80,
          text: 'Contenu dans un View',
          fontSize: 16,
          bold: true
        }),
        
        ui.Button({
          width: 150,
          height: 40,
          text: 'Bouton dans View',
          onClick: () => {
            framework.showToast('Bouton dans View cliqué!');
          }
        }),
        
        ui.Switch({
          checked: true,
          onChange: (checked) => {
            framework.showToast(`Switch dans View: ${checked ? 'ON' : 'OFF'}`);
          }
        })
      ]),
      
      // 7. ContextMenu Section
      ui.Text({
        x: 20,
        y: yPosition + 1020,
        width: framework.width - 40,
        text: '7. ContextMenu:',
        fontSize: 18,
        bold: true
      }),
      
      ui.Button({
        x: 20,
        y: yPosition + 1050,
        width: 200,
        height: 50,
        text: 'Ouvrir Menu Contextuel',
        onClick: () => {
          const menu = new ContextMenu(framework, {
            x: 20,
            y: yPosition + 1110,
            width: 200,
            options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
            onSelect: (index) => {
              framework.showToast(`Option ${index + 1} sélectionnée`);
            }
          });
          framework.add(menu);
        }
      }),
      
      // 8. Input Section
      ui.Text({
        x: 20,
        y: yPosition + 1130,
        width: framework.width - 40,
        text: '8. Input avec valeur:',
        fontSize: 18,
        bold: true
      }),
      
      // ✅ Input avec ref
      ui.Input({
        ref: testInputRef,
        x: 20,
        y: yPosition + 1160,
        width: framework.width - 40,
        height: 50,
        placeholder: 'Tapez quelque chose...',
        value: ''
      }),
      
      ui.Button({
        x: 20,
        y: yPosition + 1230,
        width: 200,
        height: 50,
        text: 'Afficher valeur',
        onClick: () => {
          const value = testInputRef.current?.value || '(vide)';
          framework.showToast(`Valeur: ${value}`);
        }
      }),
      
      // 9. Select Section
      ui.Text({
        x: 20,
        y: yPosition + 1300,
        width: framework.width - 40,
        text: '9. Select avec callback:',
        fontSize: 18,
        bold: true
      }),
      
      ui.Select({
        x: 20,
        y: yPosition + 1330,
        width: framework.width - 40,
        height: 50,
        options: ['Pomme', 'Banane', 'Orange', 'Fraise'],
        selectedIndex: 0,
        onChange: (value, index) => {
          framework.showToast(`Sélectionné: ${value} (index: ${index})`);
        }
      }),
      
      // 10. Switch Section
      ui.Text({
        x: 20,
        y: yPosition + 1410,
        width: framework.width - 40,
        text: '10. Switch avec état:',
        fontSize: 18,
        bold: true
      }),
      
      // ✅ Switch avec ref
      ui.Switch({
        ref: testSwitchRef,
        x: 20,
        y: yPosition + 1440,
        checked: false,
        onChange: (checked) => {
          framework.showToast(`Switch: ${checked ? 'ACTIVÉ' : 'DÉSACTIVÉ'}`);
        }
      }),
      
      // 11. Slider Section
      ui.Text({
        x: 20,
        y: yPosition + 1500,
        width: framework.width - 40,
        text: '11. Slider avec valeur:',
        fontSize: 18,
        bold: true
      }),
      
      // ✅ Text du slider avec ref
      ui.Text({
        ref: sliderDisplayRef,
        x: 20,
        y: yPosition + 1530,
        width: framework.width - 40,
        text: 'Valeur: 50',
        fontSize: 14
      }),
      
      ui.Slider({
        x: 20,
        y: yPosition + 1560,
        width: framework.width - 40,
        height: 40,
        value: 50,
        onChange: (value) => {
          if (sliderDisplayRef.current) {
            sliderDisplayRef.current.text = `Valeur: ${Math.round(value)}`;
            sliderDisplayRef.current.markDirty();
          }
        }
      }),
      
      // FAB flottant
      ui.FAB({
        icon: '+',
        variant: 'medium',
        x: framework.width - 56,
        y: framework.height - 56,
        bgColor: '#6750A4',
        onClick: () => {
          framework.showToast('FAB cliqué!');
        }
      }),
      
      // Bouton pour remonter
      ui.Button({
        x: framework.width / 2 - 100,
        y: yPosition + 1640,
        width: 200,
        height: 50,
        text: '↑ Remonter ↑',
        onClick: () => {
          framework.scrollOffset = 0;
        }
      })
      
    ])
  ).mount(framework);
});

// Lancer l'app
app.navigate('/', { transition: 'none' });

📄 License

MIT