diff --git a/pigway_frame_flow/web/style.css b/pigway_frame_flow/web/style.css index ca059ab..601e967 100644 --- a/pigway_frame_flow/web/style.css +++ b/pigway_frame_flow/web/style.css @@ -198,3 +198,23 @@ form#weather-form{display:grid;grid-template-columns:minmax(160px,1fr) minmax(22 #weather-form input,#weather-form select{height:46px} #image-host-form>button{align-self:flex-start} +/* Keep short connection fields together while leaving long content room to grow. */ +form:is(#ai-settings-form,#image-ai-config,#client-form,#hook-form,#provider-form,#mqtt-config){display:grid;grid-template-columns:repeat(3,minmax(0,1fr));align-items:start} +form:is(#ai-settings-form,#image-ai-config,#client-form,#hook-form,#provider-form,#mqtt-config)>.field{min-width:0} +form:is(#ai-settings-form,#image-ai-config,#client-form,#hook-form,#provider-form,#mqtt-config)>:is(button,p,.check-row,.form-grid){grid-column:1/-1;justify-self:start} +form:is(#ai-settings-form,#image-ai-config,#client-form,#hook-form,#provider-form,#mqtt-config)>button,#image-ai-generate>button{align-self:flex-start;justify-self:start} +#storage-policy>.form-grid{grid-template-columns:repeat(3,minmax(0,1fr));max-width:840px} +form#auto-collage-form{display:grid;grid-template-columns:minmax(200px,1fr) 140px minmax(200px,1fr);align-items:start} +#auto-collage-form>.form-grid{display:contents} +#auto-collage-form>.field:has(textarea),#auto-collage-form>:is(.check-row,.hint,.actions){grid-column:1/-1} +#auto-collage-form .field{min-width:0} +#auto-collage-form input:not([type=checkbox]),#auto-collage-form select{height:46px} +#auto-collage-form textarea{min-height:110px} +form#client-form{grid-template-columns:repeat(4,minmax(0,1fr))} +#client-form>.field{grid-column:span 2} +form#client-form>.check-row{grid-column:auto} +form#mqtt-config{grid-template-columns:minmax(160px,1fr) 110px minmax(180px,1.5fr) minmax(140px,1fr)} +@media(max-width:1050px){form:is(#ai-settings-form,#image-ai-config,#client-form,#hook-form,#provider-form,#mqtt-config){grid-template-columns:repeat(2,minmax(0,1fr))}form#auto-collage-form{grid-template-columns:minmax(0,1fr) 140px}#auto-collage-form>.field{grid-column:1/-1}} +@media(max-width:720px){form:is(#ai-settings-form,#image-ai-config,#client-form,#hook-form,#provider-form,#mqtt-config),form#auto-collage-form,#storage-policy>.form-grid{grid-template-columns:minmax(0,1fr)}} + +@media(max-width:720px){#client-form>.field{grid-column:auto}} diff --git a/scripts/ui-layout-audit.cjs b/scripts/ui-layout-audit.cjs index b8f44cd..541aec0 100644 --- a/scripts/ui-layout-audit.cjs +++ b/scripts/ui-layout-audit.cjs @@ -9,12 +9,12 @@ const assert=require('assert');const fs=require('fs');const {chromium}=require(' await page.route('**/api/v1/integrations/synology-photos',async route=>{if(route.request().method()==='POST'){assert.deepStrictEqual(route.request().postDataJSON(),{username:'fixture-nas',password:'fixture-only'});connected=true;logins++;}await route.fulfill({json:{configured:true,connected}});}); await page.route('**/api/v1/photo-sources',async route=>{assert(route.request().method()==='GET');await route.fulfill({json:[{id:'fixture-source',name:'NAS photos / NAS 照片',kind:'local',location:'.',enabled:false,people_filter:true,orientation:'auto'}]});}); await page.goto(base);await page.locator('#username').fill(process.env.FRAME_FLOW_TEST_USERNAME);await page.locator('#password').fill(process.env.FRAME_FLOW_TEST_PASSWORD);await page.locator('#login-form button').click();await page.locator('.hero').waitFor(); - const viewports=process.env.FRAME_FLOW_VIEWPORTS?JSON.parse(process.env.FRAME_FLOW_VIEWPORTS):[1440,768,390].map(width=>({width,height:900})); + const viewports=process.env.FRAME_FLOW_VIEWPORTS?JSON.parse(process.env.FRAME_FLOW_VIEWPORTS):[1920,1440,768,390].map(width=>({width,height:900})); let checks=0;const failures=[];const out=process.env.FRAME_FLOW_SCREENSHOT_DIR;if(out)fs.mkdirSync(out,{recursive:true}); async function inspect(label){ const result=await page.evaluate(()=>{ - const root=document.querySelector('dialog[open]')||document.querySelector('#main');const controls=[...root.querySelectorAll('button,select,input:not([type=checkbox]):not([type=radio]):not([type=file]),textarea')].filter(e=>e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})&&!e.closest('[hidden]')).map(e=>({id:e.id||e.textContent.trim().slice(0,24),r:e.getBoundingClientRect()}));const close=[]; - for(let i=0;i1&&dy<7.5)||(y>1&&dx<7.5))close.push([controls[i].id,controls[j].id]);} + const root=document.querySelector('dialog[open]')||document.querySelector('#main');const controls=[...root.querySelectorAll('button,select,input:not([type=checkbox]):not([type=radio]):not([type=file]),textarea')].filter(e=>e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})&&!e.closest('[hidden]')).map(e=>({id:e.id||e.textContent.trim().slice(0,24),compact:!!e.closest('.image-card .actions'),r:e.getBoundingClientRect()}));const close=[]; + for(let i=0;i1&&dy1&&dxinnerWidth||rect.top<0||rect.bottom>innerHeight); const clippedNav=innerWidth<=720&&[...document.querySelectorAll('nav button')].some(button=>{const r=button.getBoundingClientRect();return button.scrollHeight>button.clientHeight||r.height<44;}); @@ -25,7 +25,7 @@ const assert=require('assert');const fs=require('fs');const {chromium}=require(' await page.setViewportSize({width,height});await page.evaluate(({lang,theme})=>{state.lang=lang;document.documentElement.dataset.theme=theme;shell();},{lang,theme}); for(const view of ['dashboard','gallery','photo-sources','upload','templates','automation','playlists','history','settings','studio','connections']){await page.evaluate(view=>navigate(view),view);await inspect(`${view}/${width}/${lang}/${theme}`);} await page.evaluate(()=>navigate('studio'));await page.locator('.manual-collage summary').click();await inspect(`Manual collage/${width}/${lang}/${theme}`);await page.evaluate(()=>navigate('photo-sources'));await page.locator('#photos-login').click();await inspect(`Photos login/${width}/${lang}/${theme}`);await page.locator('#modal-cancel').click(); - await page.locator('#add-photo-source').click();assert(await page.locator('#source-credential').isHidden());assert(await page.locator('#source-oauth').isHidden());await inspect(`NAS source/${width}/${lang}/${theme}`);await page.locator('#source-kind').selectOption('google-drive');assert(await page.locator('#source-oauth').isVisible());assert(await page.locator('#source-local-quality').isHidden());await inspect(`Drive source/${width}/${lang}/${theme}`);await page.locator('#modal-cancel').click(); + await page.locator('#add-photo-source').click();assert(await page.locator('#source-credential').isHidden());assert(await page.locator('#source-oauth').isHidden());await inspect(`NAS source/${width}/${lang}/${theme}`);await page.locator('#modal-cancel').click(); await page.evaluate(()=>navigate('automation'));await page.locator('#new-job').click();await inspect(`Job/${width}/${lang}/${theme}`);await page.locator('#modal-cancel').click(); await page.evaluate(()=>navigate('playlists'));await page.locator('#new-playlist').click();await inspect(`Playlist/${width}/${lang}/${theme}`);await page.locator('#modal-cancel').click(); await page.evaluate(()=>displayDialog({type:'image',image_id:1}));await inspect(`Display options/${width}/${lang}/${theme}`);await page.locator('#modal-cancel').click(); @@ -33,6 +33,6 @@ const assert=require('assert');const fs=require('fs');const {chromium}=require(' if(out&&lang==='zh'&&theme==='dark'){await page.evaluate(()=>navigate('photo-sources'));await page.screenshot({path:out+`/photos-${width}.png`,fullPage:true});} } await page.evaluate(()=>navigate('photo-sources'));await page.locator('#photos-login').click();await page.locator('#photos-login-user').fill('fixture-nas');await page.locator('#photos-login-password').fill('fixture-only');await page.locator('#photos-login-form button[type=submit]').click();await page.locator('#photos-login').filter({hasText:'Sign in again'}).waitFor();assert(logins===1);checks++; - assert.deepStrictEqual(errors,[]);assert.deepStrictEqual(failures,[],JSON.stringify(failures));console.log('Layout audit:',checks,'passed; 11 pages plus local gallery, 6 modal states, '+(viewports.length*4)+' viewport/language/theme combinations; console errors:',errors.length); + assert.deepStrictEqual(errors,[]);assert.deepStrictEqual(failures,[],JSON.stringify(failures));console.log('Layout audit:',checks,'passed; 11 pages plus local gallery, 5 modal states, '+(viewports.length*4)+' viewport/language/theme combinations; console errors:',errors.length); }finally{await browser.close();} })().catch(e=>{console.error(e.message);process.exit(1)});