4949 < div class ="flex items-center space-x-4 ">
5050 < a href ="../README.md " class ="text-xl font-bold text-purple-600 hover:text-purple-700 "> Smart (Components) Toolkit</ a >
5151 < span class ="text-gray-300 "> |</ span >
52- < span class ="text-gray-700 font-semibold "> State Editor (API)</ span >
52+ <!-- Mode Selector -->
53+ < div class ="flex bg-gray-100 rounded-lg p-0.5 ">
54+ < button @click ="editorMode = 'state' "
55+ :class ="editorMode === 'state' ? 'bg-white shadow text-purple-600' : 'text-gray-600 hover:text-gray-800' "
56+ class ="px-3 py-1 rounded-md text-sm font-medium transition ">
57+ < i class ="fas fa-play-circle mr-1 "> </ i > State Device
58+ </ button >
59+ < button @click ="editorMode = 'captured' "
60+ :class ="editorMode === 'captured' ? 'bg-white shadow text-orange-600' : 'text-gray-600 hover:text-gray-800' "
61+ class ="px-3 py-1 rounded-md text-sm font-medium transition ">
62+ < i class ="fas fa-camera mr-1 "> </ i > Captured State
63+ </ button >
64+ </ div >
65+ < span class ="text-xs text-gray-500 "> (API)</ span >
5366 </ div >
5467 < div class ="space-x-3 ">
68+ <!-- Template button for captured mode -->
69+ < button v-if ="editorMode === 'captured' " @click ="showTemplateModal = true "
70+ :class ="capturedTemplate ? 'bg-orange-600 hover:bg-orange-700' : 'bg-orange-400 hover:bg-orange-500' "
71+ class ="text-white px-3 py-1.5 rounded text-sm transition shadow-sm ">
72+ < i class ="fas fa-list-check mr-2 "> </ i > {{ capturedTemplate ? 'Template Loaded' : 'Load Template' }}
73+ </ button >
5574 < button v-if ="currentDevice " @click ="saveToDevice " class ="bg-purple-600 hover:bg-purple-700 text-white px-3 py-1.5 rounded text-sm transition shadow-sm ">
5675 < i class ="fas fa-save mr-2 "> </ i > Save to Device
5776 </ button >
@@ -361,6 +380,33 @@ <h2 class="text-xl font-bold text-gray-800">Import JSON</h2>
361380 </ div >
362381 </ div >
363382
383+ <!-- Template Modal (Captured State Mode) -->
384+ < div v-if ="showTemplateModal " class ="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 backdrop-blur-sm ">
385+ < div class ="bg-white rounded-lg shadow-xl max-w-2xl w-full flex flex-col max-h-[90vh] ">
386+ < div class ="p-4 border-b border-gray-200 flex justify-between items-center bg-orange-50 rounded-t-lg ">
387+ < h2 class ="text-xl font-bold text-gray-800 "> < i class ="fas fa-list-check mr-2 text-orange-600 "> </ i > Load Template</ h2 >
388+ < button @click ="showTemplateModal = false " class ="text-gray-400 hover:text-gray-600 transition "> < i class ="fas fa-times text-lg "> </ i > </ button >
389+ </ div >
390+ < div class ="p-4 flex-grow flex flex-col ">
391+ < p class ="text-sm text-gray-600 mb-2 "> Paste the template JSON from your State Capture Device. This defines which devices are allowed in the captured state.</ p >
392+ < textarea v-model ="templateText " class ="w-full flex-grow bg-gray-50 border border-gray-300 rounded p-3 font-mono text-xs text-gray-800 focus:border-orange-500 focus:ring-1 focus:ring-orange-500 focus:outline-none min-h-[200px] " placeholder ='{ "items": [ { "device_id": "...", "device_name": "...", "capabilities": [...] } ] } '> </ textarea >
393+
394+ < div v-if ="templateError " class ="mt-2 text-red-600 text-sm bg-red-50 p-2 rounded border border-red-200 ">
395+ < i class ="fas fa-exclamation-circle mr-1 "> </ i > {{ templateError }}
396+ </ div >
397+
398+ < div v-if ="capturedTemplate " class ="mt-2 text-green-600 text-sm bg-green-50 p-2 rounded border border-green-200 ">
399+ < i class ="fas fa-check-circle mr-1 "> </ i > Template loaded: {{ capturedTemplate.items?.length || 0 }} devices
400+ </ div >
401+ </ div >
402+ < div class ="p-4 border-t border-gray-200 flex justify-end space-x-3 bg-orange-50 rounded-b-lg ">
403+ < button @click ="loadTemplate " class ="bg-orange-600 hover:bg-orange-700 text-white px-6 py-2 rounded text-sm font-semibold shadow-sm transition ">
404+ Load Template
405+ </ button >
406+ </ div >
407+ </ div >
408+ </ div >
409+
364410 <!-- Homey Selection Modal -->
365411 < div v-if ="showHomeyModal " class ="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4 backdrop-blur-sm ">
366412 < div class ="bg-white rounded-lg shadow-xl max-w-md w-full flex flex-col ">
@@ -423,13 +469,22 @@ <h2 class="text-xl font-bold text-gray-800">Select State Device</h2>
423469
424470 createApp ( {
425471 setup ( ) {
472+ // Editor Mode: 'state' (State Device) or 'captured' (Captured State)
473+ const editorMode = ref ( 'state' ) ;
474+
475+ // Template for captured state mode (devices allowed in state)
476+ const capturedTemplate = ref ( null ) ;
477+ const showTemplateModal = ref ( false ) ;
478+ const templateText = ref ( '' ) ;
479+ const templateError = ref ( null ) ;
480+
426481 // Global Config
427482 const config = ref ( {
428483 default_delay : 200 ,
429484 ignore_errors : true ,
430485 debug : false
431486 } ) ;
432-
487+
433488 // Editor State
434489 const zones = ref ( { } ) ;
435490 const zoneNameKeys = ref ( { } ) ;
@@ -889,6 +944,21 @@ <h2 class="text-xl font-bold text-gray-800">Select State Device</h2>
889944 } ;
890945
891946
947+ // Template Loading for Captured State Mode
948+ const loadTemplate = ( ) => {
949+ templateError . value = null ;
950+ try {
951+ const data = JSON . parse ( templateText . value ) ;
952+ if ( ! data . items || ! Array . isArray ( data . items ) ) {
953+ throw new Error ( "Invalid template: missing 'items' array" ) ;
954+ }
955+ capturedTemplate . value = data ;
956+ showTemplateModal . value = false ;
957+ } catch ( e ) {
958+ templateError . value = e . message ;
959+ }
960+ } ;
961+
892962 // Import Logic
893963 const processImport = ( mode ) => {
894964 importError . value = null ;
@@ -908,6 +978,24 @@ <h2 class="text-xl font-bold text-gray-800">Select State Device</h2>
908978 throw new Error ( "Invalid JSON: No 'zones' or 'items' found." ) ;
909979 }
910980
981+ // Validate against template in captured mode
982+ if ( editorMode . value === 'captured' && capturedTemplate . value ) {
983+ const allowedIds = new Set ( capturedTemplate . value . items . map ( item => item . device_id ) ) ;
984+ const invalidDevices = [ ] ;
985+
986+ for ( const [ zoneName , zoneData ] of Object . entries ( importedZones ) ) {
987+ for ( const item of ( zoneData . items || [ ] ) ) {
988+ if ( ! allowedIds . has ( item . id ) ) {
989+ invalidDevices . push ( item . name || item . id ) ;
990+ }
991+ }
992+ }
993+
994+ if ( invalidDevices . length > 0 ) {
995+ throw new Error ( `Devices not in template: ${ invalidDevices . join ( ', ' ) } ` ) ;
996+ }
997+ }
998+
911999 if ( mode === 'replace' ) {
9121000 config . value = { ...config . value , ...importedConfig } ;
9131001 zones . value = importedZones ;
@@ -917,7 +1005,7 @@ <h2 class="text-xl font-bold text-gray-800">Select State Device</h2>
9171005 if ( importedConfig ) {
9181006 config . value = { ...config . value , ...importedConfig } ;
9191007 }
920-
1008+
9211009 for ( const [ zName , zData ] of Object . entries ( importedZones ) ) {
9221010 if ( ! zones . value [ zName ] ) {
9231011 zones . value [ zName ] = zData ;
@@ -1110,22 +1198,23 @@ <h2 class="text-xl font-bold text-gray-800">Select State Device</h2>
11101198 return {
11111199 config, zones, zoneNameKeys,
11121200 showConfig, showInactive, showImportModal, showApiConfig, showHomeyModal, showDeviceModal, importText, importError,
1113-
1201+ editorMode, capturedTemplate, showTemplateModal, templateText, templateError,
1202+
11141203 // Auth
11151204 clientId, clientSecret, hasCredentials, isLoggedIn, userName, userAvatar,
11161205 scopeMode, recommendedScopesText, grantedScopesText, activeAuthStrategy,
11171206 saveCredentials, resetCredentials, login, logout,
1118-
1207+
11191208 // API
11201209 isFetching, apiMessage, apiError, homeys, homeyDevices, fetchDevices, selectHomey, selectDevice,
11211210 deviceSearchQuery, filteredDevices, currentDevice, saveToDevice,
1122-
1211+
11231212 simulationState, isValid, generatedJson, getItemDomId,
11241213 toggleConfig, addZone, renameZone, deleteZone,
11251214 addItem : ( ) => { } , removeItem, moveItem,
11261215 addCapability, removeCapability, moveCapability,
11271216 onDragStart, onDragEnd, onDropItem,
1128- processImport, copyToClipboard,
1217+ processImport, copyToClipboard, loadTemplate ,
11291218 startSimulation, stopSimulation
11301219 } ;
11311220 }
0 commit comments