canvasframework 0.3.13 → 0.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,27 @@
1
- <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/feae14e9-8d68-41f9-9e33-e444f1a6f360" />
1
+ <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/feae14e9-8d68-41f9-9e33-e444f1a6f360" />
2
2
 
3
3
 
4
- # Canvas UI Engine (working name)
4
+ # Canvas UI Engine (UI engine inspired by Flutter, built for the Web)
5
5
 
6
6
  > **Canvas-based UI Engine for Mobile & Embedded Apps**
7
7
  > A high-performance UI engine rendered with Canvas/WebGL, running inside a WebView runtime (Capacitor / Cordova), without DOM, HTML or CSS.
8
8
 
9
9
  ---
10
10
 
11
+ # Installation
12
+ - npm install canvasframework
13
+ - npm install -D vite
14
+
15
+ # add in package.json
16
+ "scripts": {
17
+ "dev": "vite",
18
+ "build": "vite build"
19
+ }
20
+
21
+ # launch
22
+ npm run dev
23
+ ---
24
+
11
25
  ## 🚀 Overview
12
26
 
13
27
  **Canvas UI Engine** is a **low-level UI engine** that renders the entire user interface using **Canvas 2D or WebGL**, instead of the DOM.
@@ -549,6 +563,711 @@ app.add(screen);
549
563
  screen.layoutRecursive();
550
564
  ```
551
565
 
566
+ ## UIBuilder
567
+ with UIBuilder you can build your interface with more simplicity
568
+
569
+ ```
570
+ 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';
571
+
572
+ const app = new CanvasFramework('app-canvas',{
573
+ useWebGL: true,
574
+ showFps: true,
575
+ debug: true,
576
+ });
577
+
578
+ app.useWebGL = true;
579
+
580
+ // Route principale
581
+ app.route('/', (framework) => {
582
+ const platform = framework.platform === 'material' ? 'Material Design' : 'Cupertino (iOS)';
583
+
584
+ if (framework.useWebGL) {
585
+ console.log("✅ WebGL est activé");
586
+ } else {
587
+ console.log("⚠️ WebGL non disponible, fallback en Canvas 2D");
588
+ }
589
+
590
+ // ✅ Créer une ref pour le texte du slider
591
+ const sliderValueTextRef = createRef();
592
+
593
+ ui.app(
594
+ ui.Column({ x: 0, y: 0, spacing: 0 }, [
595
+ // Titre
596
+ ui.Text({
597
+ x: 20,
598
+ y: 50,
599
+ width: framework.width - 40,
600
+ text: `Canvas Framework - ${platform}`,
601
+ fontSize: 24,
602
+ bold: true,
603
+ align: 'center'
604
+ }),
605
+
606
+ // Bouton Toast
607
+ ui.Button({
608
+ x: framework.width / 2 - 100,
609
+ y: 120,
610
+ width: 200,
611
+ height: 50,
612
+ text: 'Afficher Toast',
613
+ onClick: () => {
614
+ framework.showToast('Toast affiché avec succès!', 2000);
615
+ }
616
+ }),
617
+
618
+ // Input
619
+ ui.Input({
620
+ x: 20,
621
+ y: 200,
622
+ width: framework.width - 40,
623
+ height: 50,
624
+ placeholder: 'Entrez du texte...'
625
+ }),
626
+
627
+ // Label Slider avec ref
628
+ ui.Text({
629
+ ref: sliderValueTextRef, // ✅ Utiliser ref au lieu de onMount
630
+ x: 20,
631
+ y: 280,
632
+ width: framework.width - 40,
633
+ text: 'Valeur: 50',
634
+ fontSize: 14,
635
+ color: '#666666'
636
+ }),
637
+
638
+ // Slider
639
+ ui.Slider({
640
+ x: 20,
641
+ y: 310,
642
+ width: framework.width - 40,
643
+ height: 40,
644
+ value: 50,
645
+ onChange: (value) => {
646
+ if (sliderValueTextRef.current) {
647
+ sliderValueTextRef.current.text = `Valeur: ${Math.round(value)}`;
648
+ sliderValueTextRef.current.markDirty();
649
+ }
650
+ }
651
+ }),
652
+
653
+ // Label Select
654
+ ui.Text({
655
+ x: 20,
656
+ y: 380,
657
+ width: framework.width - 40,
658
+ text: 'Menu déroulant:',
659
+ fontSize: 14,
660
+ color: '#666666'
661
+ }),
662
+
663
+ // Select
664
+ ui.Select({
665
+ x: 20,
666
+ y: 410,
667
+ width: framework.width - 40,
668
+ height: 50,
669
+ options: ['Option 1', 'Option 2', 'Option 3'],
670
+ selectedIndex: 0,
671
+ onChange: (selectedOption, selectedIndex) => {
672
+ console.log(`Option sélectionnée : ${selectedOption} (index: ${selectedIndex})`);
673
+ }
674
+ }),
675
+
676
+ // Label Switch
677
+ ui.Text({
678
+ x: 20,
679
+ y: 490,
680
+ width: framework.width - 100,
681
+ text: 'Activer notifications',
682
+ fontSize: 16
683
+ }),
684
+
685
+ // Switch
686
+ ui.Switch({
687
+ x: framework.width - 71,
688
+ y: 485,
689
+ checked: false,
690
+ onChange: (checked) => {
691
+ framework.showToast(checked ? 'Notifications activées' : 'Notifications désactivées');
692
+ }
693
+ }),
694
+
695
+ // Bouton Page 2
696
+ ui.Button({
697
+ x: 20,
698
+ y: 550,
699
+ type: 'outlined',
700
+ shape: 'square',
701
+ width: framework.width / 2 - 30,
702
+ height: 50,
703
+ text: 'Page 2 →',
704
+ bgColor: framework.platform === 'material' ? '#03DAC6' : '#FF9500',
705
+ onClick: () => {
706
+ framework.navigate('/page2', { transition: 'fade' });
707
+ }
708
+ }),
709
+
710
+ // Bouton Tout Tester
711
+ ui.Button({
712
+ x: framework.width / 2 + 10,
713
+ y: 550,
714
+ width: framework.width / 2 - 30,
715
+ height: 50,
716
+ type: 'filled',
717
+ shape: 'rounded',
718
+ text: 'Tout Tester →',
719
+ bgColor: framework.platform === 'material' ? '#FF9800' : '#34C759',
720
+ onClick: () => {
721
+ framework.navigate('/test', { transition: 'slide' });
722
+ }
723
+ })
724
+ ])
725
+ ).mount(framework);
726
+ });
727
+
728
+ // Page 2 avec Drawer et Navigation
729
+ app.route('/page2', (framework) => {
730
+ // ✅ Créer une ref pour le drawer
731
+ const drawerRef = createRef();
732
+
733
+ ui.app(
734
+ ui.Column({ x: 0, y: 0, spacing: 0 }, [
735
+
736
+ // Drawer avec ref
737
+ ui.Drawer({
738
+ ref: drawerRef, // ✅ Utiliser ref
739
+ header: { title: 'Mon App' },
740
+ items: [
741
+ { icon: '🏠', label: 'Accueil' },
742
+ { icon: '⚙️', label: 'Paramètres' },
743
+ { icon: '❤️', label: 'Favoris', divider: true },
744
+ { icon: '👤', label: 'Profil' }
745
+ ],
746
+ onItemClick: (index, item) => {
747
+ framework.showToast(`${item.label} cliqué`);
748
+ framework.navigate('/', { transition: 'none' });
749
+ }
750
+ }),
751
+
752
+ // AppBar (sera au-dessus)
753
+ ui.AppBar({
754
+ title: 'Accueil',
755
+ leftIcon: 'menu',
756
+ rightIcon: 'search',
757
+ onLeftClick: () => {
758
+ if (drawerRef.current) drawerRef.current.open();
759
+ },
760
+ onRightClick: () => {
761
+ framework.showToast('Recherche');
762
+ }
763
+ }),
764
+
765
+ // PasswordInput
766
+ ui.PasswordInput({
767
+ placeholder: 'Entrez votre mot de passe',
768
+ value: '',
769
+ fontSize: 16,
770
+ x: 20,
771
+ y: 120,
772
+ width: 300,
773
+ height: 40,
774
+ maskChar: '•',
775
+ showPassword: false,
776
+ onFocus: () => {
777
+ console.log('PasswordInput focus');
778
+ },
779
+ onBlur: () => {
780
+ console.log('PasswordInput blur');
781
+ }
782
+ }),
783
+
784
+ // InputTags
785
+ ui.InputTags({
786
+ placeholder: 'Ajouter des tags...',
787
+ tags: ['javascript', 'canvas'],
788
+ x: 20,
789
+ y: 160,
790
+ width: 300,
791
+ height: 50,
792
+ tagColor: '#E3F2FD',
793
+ tagTextColor: '#1565C0',
794
+ deleteButtonColor: '#1565C0',
795
+ onTagAdd: (tag, allTags) => {
796
+ console.log('Tag ajouté:', tag, 'Tags:', allTags);
797
+ },
798
+ onTagRemove: (tag, allTags) => {
799
+ console.log('Tag supprimé:', tag, 'Tags:', allTags);
800
+ }
801
+ }),
802
+
803
+ // InputDatalist
804
+ ui.InputDatalist({
805
+ placeholder: 'Sélectionnez un pays...',
806
+ value: '',
807
+ options: [
808
+ 'France', 'Allemagne', 'Espagne', 'Italie', 'Portugal',
809
+ 'Belgique', 'Suisse', 'Canada', 'États-Unis', 'Japon',
810
+ 'Chine', 'Corée du Sud', 'Australie', 'Brésil', 'Mexique'
811
+ ],
812
+ x: 50,
813
+ y: 210,
814
+ width: 300,
815
+ height: 40,
816
+ maxDropdownItems: 8,
817
+ dropdownBackground: '#FFFFFF',
818
+ hoverBackground: '#F0F0F0',
819
+ selectedBackground: '#E3F2FD',
820
+ borderColor: '#CCCCCC',
821
+ onSelect: (selectedValue) => {
822
+ console.log('Option sélectionnée:', selectedValue);
823
+ },
824
+ onInput: (currentValue) => {
825
+ console.log('Valeur en cours:', currentValue);
826
+ }
827
+ }),
828
+
829
+ // BottomNavigationBar
830
+ ui.BottomNavigationBar({
831
+ items: [
832
+ { icon: 'home', label: 'Home' },
833
+ { icon: 'search', label: 'Search' },
834
+ { icon: 'favorite', label: 'Favorites' },
835
+ { icon: 'person', label: 'Profile' },
836
+ { icon: 'settings', label: 'Settings' }
837
+ ],
838
+ selectedIndex: 0,
839
+ selectedColor: '#6200EE',
840
+ onChange: (index, item) => {
841
+ console.log(`Tab changed to: ${item.label} (index ${index})`);
842
+ switch(index) {
843
+ case 0: framework.navigate('home'); break;
844
+ case 1: framework.navigate('search'); break;
845
+ case 2: framework.navigate('favorites'); break;
846
+ case 3: framework.navigate('profile'); break;
847
+ case 4: framework.navigate('settings'); break;
848
+ }
849
+ }
850
+ })
851
+
852
+ ])
853
+ ).mount(framework);
854
+ });
855
+
856
+ // Page de test complète
857
+ app.route('/test', (framework) => {
858
+ // ✅ Créer toutes les refs nécessaires
859
+ const progressBarRef = createRef();
860
+ const testInputRef = createRef();
861
+ const sliderDisplayRef = createRef();
862
+ const testSwitchRef = createRef();
863
+
864
+ let yPosition = 80;
865
+
866
+ ui.app(
867
+ ui.Column({ x: 0, y: 0, spacing: 0 }, [
868
+
869
+ // AppBar de retour (fixe)
870
+ ui.AppBar({
871
+ title: 'Test Complet',
872
+ leftIcon: 'back',
873
+ onLeftClick: () => {
874
+ framework.navigate('/', { transition: 'slide' });
875
+ }
876
+ }),
877
+
878
+ // Titre principal
879
+ ui.Text({
880
+ x: 20,
881
+ y: yPosition,
882
+ width: framework.width - 40,
883
+ text: 'Test de tous les composants',
884
+ fontSize: 24,
885
+ bold: true,
886
+ align: 'center'
887
+ }),
888
+
889
+ // 1. ProgressBar Section
890
+ ui.Text({
891
+ x: 20,
892
+ y: yPosition + 60,
893
+ width: framework.width - 40,
894
+ text: '1. ProgressBar:',
895
+ fontSize: 18,
896
+ bold: true
897
+ }),
898
+
899
+ // ✅ ProgressBar avec ref - PLUS BESOIN de création manuelle !
900
+ ui.ProgressBar({
901
+ ref: progressBarRef,
902
+ x: 20,
903
+ y: yPosition + 90,
904
+ width: framework.width - 40,
905
+ progress: 30
906
+ }),
907
+
908
+ ui.Button({
909
+ x: 20,
910
+ y: yPosition + 140,
911
+ width: 150,
912
+ height: 40,
913
+ text: 'Augmenter',
914
+ onClick: () => {
915
+ if (progressBarRef.current) {
916
+ progressBarRef.current.progress = Math.min(100, progressBarRef.current.progress + 10);
917
+ progressBarRef.current.markDirty();
918
+ }
919
+ }
920
+ }),
921
+
922
+ // 2. RadioButton Section
923
+ ui.Text({
924
+ x: 20,
925
+ y: yPosition + 200,
926
+ width: framework.width - 40,
927
+ text: '2. RadioButton (Groupe 1):',
928
+ fontSize: 18,
929
+ bold: true
930
+ }),
931
+
932
+ ui.RadioButton({
933
+ x: 40,
934
+ y: yPosition + 230,
935
+ group: 'groupe1',
936
+ label: 'Option A',
937
+ checked: true,
938
+ onChange: (checked) => {
939
+ if (checked) framework.showToast('Radio A sélectionné');
940
+ }
941
+ }),
942
+
943
+ ui.RadioButton({
944
+ x: 40,
945
+ y: yPosition + 270,
946
+ group: 'groupe1',
947
+ label: 'Option B',
948
+ onChange: (checked) => {
949
+ if (checked) framework.showToast('Radio B sélectionné');
950
+ }
951
+ }),
952
+
953
+ // 3. Checkbox Section
954
+ ui.Text({
955
+ x: 20,
956
+ y: yPosition + 330,
957
+ width: framework.width - 40,
958
+ text: '3. Checkbox:',
959
+ fontSize: 18,
960
+ bold: true
961
+ }),
962
+
963
+ ui.Checkbox({
964
+ x: 40,
965
+ y: yPosition + 360,
966
+ label: 'Accepter les termes',
967
+ checked: false,
968
+ onChange: (checked) => {
969
+ framework.showToast(checked ? 'Coché' : 'Décoché');
970
+ }
971
+ }),
972
+
973
+ // 4. Card Section
974
+ ui.Text({
975
+ x: 20,
976
+ y: yPosition + 420,
977
+ width: framework.width - 40,
978
+ text: '4. Card:',
979
+ fontSize: 18,
980
+ bold: true
981
+ }),
982
+
983
+ ui.Card({
984
+ x: 20,
985
+ y: yPosition + 450,
986
+ width: framework.width - 40,
987
+ height: 180,
988
+ padding: 16,
989
+ elevation: 4,
990
+ borderRadius: 8
991
+ }, [
992
+ ui.Text({
993
+ x: 0,
994
+ y: 0,
995
+ width: framework.width - 72,
996
+ text: 'Titre de la carte',
997
+ fontSize: 18,
998
+ bold: true
999
+ }),
1000
+
1001
+ ui.Text({
1002
+ x: 0,
1003
+ y: 40,
1004
+ width: framework.width - 72,
1005
+ maxWidth: framework.width - 72,
1006
+ 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.',
1007
+ fontSize: 14,
1008
+ color: '#666666',
1009
+ wrap: true
1010
+ }),
1011
+
1012
+ ui.Button({
1013
+ x: 0,
1014
+ y: 120,
1015
+ width: 150,
1016
+ height: 40,
1017
+ text: 'Bouton dans Card',
1018
+ onClick: () => {
1019
+ framework.showToast('Bouton dans Card cliqué!');
1020
+ }
1021
+ })
1022
+ ]),
1023
+
1024
+ // 5. Dialog Section
1025
+ ui.Text({
1026
+ x: 20,
1027
+ y: yPosition + 650,
1028
+ width: framework.width - 40,
1029
+ text: '5. Dialog:',
1030
+ fontSize: 18,
1031
+ bold: true
1032
+ }),
1033
+
1034
+ ui.Button({
1035
+ x: 20,
1036
+ y: yPosition + 680,
1037
+ width: 200,
1038
+ height: 50,
1039
+ text: 'Afficher Dialog',
1040
+ onClick: () => {
1041
+ const dialog = new Dialog(framework, {
1042
+ title: 'Titre du Dialog',
1043
+ message: 'Ceci est un message de dialog. Voulez-vous continuer ?',
1044
+ buttons: ['Annuler', 'OK'],
1045
+ onButtonClick: (index, text) => {
1046
+ framework.showToast(`Bouton cliqué: ${text}`);
1047
+ }
1048
+ });
1049
+ framework.add(dialog);
1050
+ dialog.show();
1051
+ }
1052
+ }),
1053
+
1054
+ // 6. View Section
1055
+ ui.Text({
1056
+ x: 20,
1057
+ y: yPosition + 760,
1058
+ width: framework.width - 40,
1059
+ text: '6. View (Container):',
1060
+ fontSize: 18,
1061
+ bold: true
1062
+ }),
1063
+
1064
+ ui.View({
1065
+ x: 20,
1066
+ y: yPosition + 790,
1067
+ width: framework.width - 40,
1068
+ height: 200,
1069
+ padding: 20,
1070
+ gap: 10,
1071
+ direction: 'column',
1072
+ bgColor: '#F0F0F0',
1073
+ borderRadius: 8
1074
+ }, [
1075
+ ui.Text({
1076
+ width: framework.width - 80,
1077
+ text: 'Contenu dans un View',
1078
+ fontSize: 16,
1079
+ bold: true
1080
+ }),
1081
+
1082
+ ui.Button({
1083
+ width: 150,
1084
+ height: 40,
1085
+ text: 'Bouton dans View',
1086
+ onClick: () => {
1087
+ framework.showToast('Bouton dans View cliqué!');
1088
+ }
1089
+ }),
1090
+
1091
+ ui.Switch({
1092
+ checked: true,
1093
+ onChange: (checked) => {
1094
+ framework.showToast(`Switch dans View: ${checked ? 'ON' : 'OFF'}`);
1095
+ }
1096
+ })
1097
+ ]),
1098
+
1099
+ // 7. ContextMenu Section
1100
+ ui.Text({
1101
+ x: 20,
1102
+ y: yPosition + 1020,
1103
+ width: framework.width - 40,
1104
+ text: '7. ContextMenu:',
1105
+ fontSize: 18,
1106
+ bold: true
1107
+ }),
1108
+
1109
+ ui.Button({
1110
+ x: 20,
1111
+ y: yPosition + 1050,
1112
+ width: 200,
1113
+ height: 50,
1114
+ text: 'Ouvrir Menu Contextuel',
1115
+ onClick: () => {
1116
+ const menu = new ContextMenu(framework, {
1117
+ x: 20,
1118
+ y: yPosition + 1110,
1119
+ width: 200,
1120
+ options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'],
1121
+ onSelect: (index) => {
1122
+ framework.showToast(`Option ${index + 1} sélectionnée`);
1123
+ }
1124
+ });
1125
+ framework.add(menu);
1126
+ }
1127
+ }),
1128
+
1129
+ // 8. Input Section
1130
+ ui.Text({
1131
+ x: 20,
1132
+ y: yPosition + 1130,
1133
+ width: framework.width - 40,
1134
+ text: '8. Input avec valeur:',
1135
+ fontSize: 18,
1136
+ bold: true
1137
+ }),
1138
+
1139
+ // ✅ Input avec ref
1140
+ ui.Input({
1141
+ ref: testInputRef,
1142
+ x: 20,
1143
+ y: yPosition + 1160,
1144
+ width: framework.width - 40,
1145
+ height: 50,
1146
+ placeholder: 'Tapez quelque chose...',
1147
+ value: ''
1148
+ }),
1149
+
1150
+ ui.Button({
1151
+ x: 20,
1152
+ y: yPosition + 1230,
1153
+ width: 200,
1154
+ height: 50,
1155
+ text: 'Afficher valeur',
1156
+ onClick: () => {
1157
+ const value = testInputRef.current?.value || '(vide)';
1158
+ framework.showToast(`Valeur: ${value}`);
1159
+ }
1160
+ }),
1161
+
1162
+ // 9. Select Section
1163
+ ui.Text({
1164
+ x: 20,
1165
+ y: yPosition + 1300,
1166
+ width: framework.width - 40,
1167
+ text: '9. Select avec callback:',
1168
+ fontSize: 18,
1169
+ bold: true
1170
+ }),
1171
+
1172
+ ui.Select({
1173
+ x: 20,
1174
+ y: yPosition + 1330,
1175
+ width: framework.width - 40,
1176
+ height: 50,
1177
+ options: ['Pomme', 'Banane', 'Orange', 'Fraise'],
1178
+ selectedIndex: 0,
1179
+ onChange: (value, index) => {
1180
+ framework.showToast(`Sélectionné: ${value} (index: ${index})`);
1181
+ }
1182
+ }),
1183
+
1184
+ // 10. Switch Section
1185
+ ui.Text({
1186
+ x: 20,
1187
+ y: yPosition + 1410,
1188
+ width: framework.width - 40,
1189
+ text: '10. Switch avec état:',
1190
+ fontSize: 18,
1191
+ bold: true
1192
+ }),
1193
+
1194
+ // ✅ Switch avec ref
1195
+ ui.Switch({
1196
+ ref: testSwitchRef,
1197
+ x: 20,
1198
+ y: yPosition + 1440,
1199
+ checked: false,
1200
+ onChange: (checked) => {
1201
+ framework.showToast(`Switch: ${checked ? 'ACTIVÉ' : 'DÉSACTIVÉ'}`);
1202
+ }
1203
+ }),
1204
+
1205
+ // 11. Slider Section
1206
+ ui.Text({
1207
+ x: 20,
1208
+ y: yPosition + 1500,
1209
+ width: framework.width - 40,
1210
+ text: '11. Slider avec valeur:',
1211
+ fontSize: 18,
1212
+ bold: true
1213
+ }),
1214
+
1215
+ // ✅ Text du slider avec ref
1216
+ ui.Text({
1217
+ ref: sliderDisplayRef,
1218
+ x: 20,
1219
+ y: yPosition + 1530,
1220
+ width: framework.width - 40,
1221
+ text: 'Valeur: 50',
1222
+ fontSize: 14
1223
+ }),
1224
+
1225
+ ui.Slider({
1226
+ x: 20,
1227
+ y: yPosition + 1560,
1228
+ width: framework.width - 40,
1229
+ height: 40,
1230
+ value: 50,
1231
+ onChange: (value) => {
1232
+ if (sliderDisplayRef.current) {
1233
+ sliderDisplayRef.current.text = `Valeur: ${Math.round(value)}`;
1234
+ sliderDisplayRef.current.markDirty();
1235
+ }
1236
+ }
1237
+ }),
1238
+
1239
+ // FAB flottant
1240
+ ui.FAB({
1241
+ icon: '+',
1242
+ variant: 'medium',
1243
+ x: framework.width - 56,
1244
+ y: framework.height - 56,
1245
+ bgColor: '#6750A4',
1246
+ onClick: () => {
1247
+ framework.showToast('FAB cliqué!');
1248
+ }
1249
+ }),
1250
+
1251
+ // Bouton pour remonter
1252
+ ui.Button({
1253
+ x: framework.width / 2 - 100,
1254
+ y: yPosition + 1640,
1255
+ width: 200,
1256
+ height: 50,
1257
+ text: '↑ Remonter ↑',
1258
+ onClick: () => {
1259
+ framework.scrollOffset = 0;
1260
+ }
1261
+ })
1262
+
1263
+ ])
1264
+ ).mount(framework);
1265
+ });
1266
+
1267
+ // Lancer l'app
1268
+ app.navigate('/', { transition: 'none' });
1269
+ ```
1270
+
552
1271
  ## 📄 License
553
1272
 
554
1273
  MIT