fix ui composer
This commit is contained in:
1 parent
a26ae28189
commit
33930e972c
8 files changed
+73
-16
No files matched your search
@@ -1,2 +1,2 @@
|
||||
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
|
||||
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-tracking:initial;--tw-border-style:solid;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:Vazirmatn,ui-sans-serif,system-ui;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-500:oklch(63.7% .237 25.331);--color-red-700:oklch(50.5% .213 27.518);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-emerald-50:oklch(97.9% .021 166.113);--color-emerald-200:oklch(90.5% .093 164.15);--color-emerald-800:oklch(43.2% .095 166.913);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-400:oklch(70.4% .04 256.788);--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--font-weight-extrabold:800;--font-weight-black:900;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--blur-sm:8px;--blur-xl:24px;--blur-2xl:40px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-ink:#08376b;--color-ink-soft:#0b4c8c;--color-cream:#eee;--color-soft:#eaeaea;--color-paper:#fff;--color-sea:#0693e3;--color-sea-dark:#1977ce;--color-blue:#1977ce;--color-mint:#e5f4ff;--color-coral:#fa9f00;--color-sun:#ffc451;--shadow-panel:0 18px 55px -35px #08376b59}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-Line truncated
|
||||
@@ -30,6 +30,8 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
if(composer)layoutObserver?.observe(composer);
|
||||
let stableViewportHeight=Math.max(window.visualViewport?.height||window.innerHeight,window.innerHeight);
|
||||
let inputVisibilityFrame=0;
|
||||
let keyboardWasOpen=false;
|
||||
let suppressKeyboardCollapse=false;
|
||||
const ensureInputVisible=()=>{
|
||||
inputVisibilityFrame=0;
|
||||
if(!composer||!input||document.activeElement!==input||composer.classList.contains('is-collapsed'))return;
|
||||
@@ -42,20 +44,24 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
const inputFocused=document.activeElement===input;
|
||||
if(!inputFocused)stableViewportHeight=Math.max(stableViewportHeight,height);
|
||||
const keyboardOpen=inputFocused&&stableViewportHeight-height>120;
|
||||
const keyboardClosed=keyboardWasOpen&&!keyboardOpen&&stableViewportHeight-height<80;
|
||||
keyboardWasOpen=keyboardOpen||(keyboardWasOpen&&!keyboardClosed);
|
||||
composer?.classList.toggle('is-keyboard-active',keyboardOpen);
|
||||
composer?.setAttribute('data-keyboard-open',String(keyboardOpen));
|
||||
if(keyboardOpen)scheduleInputVisibility();
|
||||
if(keyboardClosed&&!suppressKeyboardCollapse&&!composer?.classList.contains('is-collapsed'))setTimeout(()=>setComposerOpen(false,false),0);
|
||||
};
|
||||
window.addEventListener('appviewportchange',event=>{if(followLatest)requestAnimationFrame(()=>scrollLatest(false));updateKeyboardState(event.detail?.height||window.visualViewport?.height||window.innerHeight)});
|
||||
input?.addEventListener('focus',()=>{updateKeyboardState(window.visualViewport?.height||window.innerHeight);scheduleInputVisibility()});
|
||||
input?.addEventListener('blur',()=>setTimeout(()=>updateKeyboardState(window.visualViewport?.height||window.innerHeight),0));
|
||||
input?.addEventListener('blur',()=>setTimeout(()=>{composer?.classList.remove('is-keyboard-active');composer?.setAttribute('data-keyboard-open','false')},0));
|
||||
composer?.addEventListener('pointerdown',event=>{if(!composer.classList.contains('is-collapsed')&&!event.target.closest('[data-composer-close]')){suppressKeyboardCollapse=true;setTimeout(()=>{suppressKeyboardCollapse=false},1200)}},{capture:true});
|
||||
|
||||
const groupDates=()=>{scroller.querySelectorAll('.date-divider').forEach(item=>item.remove());let previousDate='';scroller.querySelectorAll('[data-message]').forEach(message=>{if(message.dataset.date!==previousDate){const divider=document.createElement('div');divider.className='date-divider';divider.innerHTML=`<span>${message.dataset.dayLabel}</span>`;message.before(divider);previousDate=message.dataset.date}})};
|
||||
groupDates();
|
||||
document.body.addEventListener('htmx:beforeSwap',event=>{if(event.detail.target?.matches('[data-load-older]')){scroller.dataset.previousHeight=scroller.scrollHeight}});
|
||||
document.body.addEventListener('htmx:afterSwap',event=>{if(event.detail.target?.matches('[data-load-older]')){const previous=Number(scroller.dataset.previousHeight||0);groupDates();scroller.scrollTop+=scroller.scrollHeight-previous}});
|
||||
|
||||
const resize=()=>{if(!input)return;input.style.height='auto';input.style.height=`${Math.min(Math.max(input.scrollHeight,112),240)}px`;scheduleInputVisibility()};
|
||||
const resize=()=>{if(!input)return;const maxHeight=window.matchMedia('(max-width:639px)').matches?144:192;input.style.height='0px';const contentHeight=input.scrollHeight;input.style.height=`${Math.min(Math.max(contentHeight,48),maxHeight)}px`;input.style.overflowY=contentHeight>maxHeight?'auto':'hidden';scheduleInputVisibility()};
|
||||
const draftKey=()=>`${baseKey}-${mode}`;
|
||||
const status=workspace.querySelector('[data-draft-status]');
|
||||
const replyResult=workspace.dataset.replyResult||new URLSearchParams(location.search).get('reply_result')||'';
|
||||
@@ -64,7 +70,7 @@ document.querySelectorAll('[data-conversation-workspace]').forEach(workspace=>{
|
||||
const setComposerOpen=(open,focus=true)=>{const keepLatestVisible=followLatest||atBottom();composer?.classList.toggle('is-collapsed',!open);composerToggle?.setAttribute('aria-expanded',String(open));[0,80,240].forEach(delay=>setTimeout(()=>{if(keepLatestVisible)scrollLatest(false)},delay));if(open&&focus)setTimeout(()=>{input?.focus({preventScroll:true});scheduleInputVisibility()},80)};
|
||||
const saveDraft=()=>{if(!input)return;sessionStorage.setItem(draftKey(),input.value);if(status){status.textContent=input.value?'پیشنویس ذخیره شد':'';clearTimeout(saveDraft.timer);saveDraft.timer=setTimeout(()=>status.textContent='',1800)}};
|
||||
const loadDraft=()=>{if(!input)return;input.value=sessionStorage.getItem(draftKey())||'';resize();input.dispatchEvent(new Event('input',{bubbles:true}))};
|
||||
if(input){input.setAttribute('rows','5');input.setAttribute('placeholder','شرح کامل پاسخ، اطلاعات تکمیلی و نتیجه مورد انتظار خود را بنویسید…');input.addEventListener('input',()=>{resize();saveDraft()});loadDraft()}
|
||||
if(input){input.setAttribute('rows','1');input.setAttribute('placeholder','پاسخ خود را بنویسید…');input.addEventListener('input',()=>{resize();saveDraft()});loadDraft()}
|
||||
const hasDraft=Boolean(sessionStorage.getItem(`${baseKey}-reply`)||sessionStorage.getItem(`${baseKey}-note`));
|
||||
if(submissionSucceeded){form?.reset();if(input){input.value='';resize()}setComposerOpen(false,false)}else setComposerOpen(hasDraft||replyResult==='error',false);
|
||||
composerToggle?.addEventListener('click',()=>setComposerOpen(true));
|
||||
|
||||
@@ -130,6 +130,11 @@ body:has(.ticket-workspace) .site-footer{display:none}body:has(.ticket-workspace
|
||||
@media(max-width:639px){.profile-completion-notice{grid-template-columns:1fr;margin-top:0}.profile-completion-icon{width:2.4rem;height:2.4rem}.profile-completion-actions{align-items:stretch;flex-direction:column;gap:.65rem}.profile-completion-actions>a{text-align:center}.pwa-install{display:grid;grid-template-columns:auto 1fr;align-items:center}.pwa-install-actions{grid-column:1/-1}.pwa-install-actions button{flex:1}}
|
||||
.composer-send-icon{cursor:pointer;user-select:none}.composer-send-icon:focus-visible{outline:3px solid rgba(6,147,227,.25);outline-offset:2px}
|
||||
|
||||
/* Compact, keyboard-safe ticket composer shared by customer, magic-link and staff views. */
|
||||
.composer-input-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:.55rem}.composer-editor{min-width:0}.composer-draft-status{display:block;min-height:.85rem;margin:.15rem .25rem 0;color:#60758b;font-size:.58rem}.composer-draft-status:empty{display:none}.composer-input-row .composer-send-icon{margin-bottom:.25rem}.ticket-workspace .composer textarea{display:block;width:100%;height:3rem;min-height:3rem;max-height:12rem;overflow-y:hidden;padding:.65rem .85rem;line-height:1.7;transition:border-color .15s ease,box-shadow .15s ease}.reply-media-picker{margin:.55rem 0;padding:.55rem}.media-picker-actions{gap:.5rem}.media-picker-button{min-height:2.5rem;gap:.45rem;border-radius:.7rem;padding:.45rem .65rem;font-size:.7rem}.media-picker-icon{display:grid;width:1.1rem;height:1.1rem;flex:none;place-items:center}.media-picker-icon svg{display:block;width:100%;height:100%;fill:currentColor}.compression-note{margin-top:.45rem}.ticket-workspace .composer.is-keyboard-active textarea{min-height:3rem;max-height:min(30svh,9rem)!important}
|
||||
@media(max-width:639px){.ticket-workspace .composer:not(.is-collapsed){padding:.6rem .65rem max(.65rem,env(safe-area-inset-bottom))}.ticket-workspace .composer-context{display:none}.composer-input-row{gap:.4rem}.composer-input-row .composer-send-icon{width:2.65rem;height:2.65rem;min-height:2.65rem;margin-bottom:.175rem}.ticket-workspace .composer textarea{height:3rem;min-height:3rem;max-height:9rem;padding:.6rem .7rem;font-size:1rem}.reply-media-picker{margin:.45rem 0;padding:.45rem}.media-picker-actions{grid-template-columns:repeat(2,minmax(0,1fr));gap:.4rem}.media-picker-button{min-height:2.4rem;gap:.3rem;padding:.35rem .4rem;font-size:.64rem;line-height:1.35;text-align:center}.media-picker-icon{width:1rem;height:1rem}.compression-note{margin-top:.35rem;font-size:.6rem}.ticket-workspace .composer-close{display:none!important}.staff-ticket-workspace .composer:not(.is-collapsed) .composer-tools{grid-template-columns:repeat(2,minmax(0,1fr))}.composer-bar{margin-top:.5rem;padding-top:.5rem}.ticket-workspace .composer-bar{bottom:calc(-1 * max(.65rem,env(safe-area-inset-bottom)));margin-bottom:calc(-1 * max(.65rem,env(safe-area-inset-bottom)));padding-bottom:max(.65rem,env(safe-area-inset-bottom))}.conversation-pane:not(:has(.composer.is-collapsed)) .new-message-jump{bottom:15rem}}
|
||||
@media(min-width:640px){.composer-input-row .composer-send-icon{width:2.75rem;height:2.75rem;min-height:2.75rem}.reply-media-picker{max-width:36rem}.media-picker-actions{grid-template-columns:repeat(2,minmax(11rem,1fr))}}
|
||||
|
||||
/* Non-blocking guidance when current-page timing/length signals resemble rapid chat. */
|
||||
.chat-coach{@apply mt-2 grid grid-cols-[auto_1fr_auto] items-start gap-2.5 rounded-xl border border-amber-400/45 bg-amber-50 px-3 py-2.5 text-right;animation:chat-coach-in .2s ease-out}.chat-coach[hidden]{display:none}.chat-coach-icon{@apply grid size-7 place-items-center rounded-lg bg-amber-400/20 text-sm text-amber-800}.chat-coach b{@apply block text-[11px] text-amber-800}.chat-coach p{@apply mt-0.5 text-[10px] leading-5 text-amber-800/80}.chat-coach button{@apply grid size-7 min-h-0 place-items-center rounded-full bg-transparent p-0 text-base font-normal text-amber-800/60 shadow-none hover:bg-amber-400/15 hover:text-amber-800}@keyframes chat-coach-in{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}
|
||||
.chat-coach-floating{position:fixed;z-index:90;left:1rem;bottom:1rem;width:min(28rem,calc(100vw - 2rem));margin:0;box-shadow:0 18px 50px -24px rgba(8,55,107,.7)}
|
||||
|
||||
@@ -12,8 +12,9 @@
|
||||
<section class="composer {% if request.GET.reply_result != 'error' %}is-collapsed{% endif %}" data-composer-shell>
|
||||
<button type="button" class="composer-launch" data-composer-toggle aria-expanded="false"><span class="composer-launch-icon">+</span><span><b>ثبت پاسخ جدید</b><small>برای افزودن توضیحات یا مستندات به این تیکت</small></span><span class="composer-launch-arrow">↑</span></button>
|
||||
<form method="post" enctype="multipart/form-data" data-ticket-composer data-chat-coach data-chat-coach-key="ticket-{{ ticket.public_id }}-customer" action="{% url 'tickets:reply' ticket.public_id %}">{% csrf_token %}
|
||||
<div class="composer-context"><div><b>ثبت پاسخ تکمیلی در تیکت</b><small>لطفاً توضیحات خود را کامل، دقیق و یکجا بنویسید تا کارشناس بتواند پاسخ مناسبی ارائه کند.</small></div><div class="composer-context-actions"><span data-draft-status></span><span class="composer-send-icon" role="button" tabindex="0" data-scroll-to-submit aria-label="رفتن به دکمه ثبت پاسخ" title="رفتن به دکمه ثبت پاسخ"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 20.5 21 12 3 3.5v6.6l12 1.9-12 1.9z"/></svg></span></div></div>
|
||||
{{ form.body }}{{ form.body.errors }}<div class="reply-media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="customer-reply-gallery">▣ انتخاب عکس از گالری</label><label class="media-picker-button" for="customer-reply-file">+ انتخاب فایل</label></div><input id="customer-reply-gallery" type="file" accept="image/*" multiple data-gallery-input><input id="customer-reply-file" type="file" multiple data-document-input><span class="canonical-files">{{ form.files }}</span>{{ form.files.errors }}<small class="compression-note" data-compression-status>تصاویر بزرگ پیش از ارسال بهینه میشوند.</small></div>
|
||||
<div class="composer-context"><div><b>ثبت پاسخ تکمیلی در تیکت</b></div></div>
|
||||
<div class="composer-input-row"><div class="composer-editor">{{ form.body }}{{ form.body.errors }}<span class="composer-draft-status" data-draft-status></span></div><span class="composer-send-icon" role="button" tabindex="0" data-scroll-to-submit aria-label="رفتن به دکمه ثبت پاسخ" title="رفتن به دکمه ثبت پاسخ"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 20.5 21 12 3 3.5v6.6l12 1.9-12 1.9z"/></svg></span></div>
|
||||
<div class="reply-media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="customer-reply-gallery"><span class="media-picker-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 5h3l1.2-1.5h7.6L17 5h3a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm8 3.2a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z"/></svg></span><span>انتخاب عکس از گالری</span></label><label class="media-picker-button" for="customer-reply-file"><span class="media-picker-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8.5 12.8 14.8 6.5a3 3 0 1 1 4.2 4.2l-8.5 8.5a5 5 0 0 1-7.1-7.1l8-8 1.4 1.4-8 8a3 3 0 1 0 4.3 4.3l8.5-8.5a1 1 0 0 0-1.4-1.4l-6.4 6.3a1 1 0 0 0 1.4 1.5l5.7-5.7 1.4 1.4-5.7 5.7a3 3 0 1 1-4.2-4.3Z"/></svg></span><span>انتخاب فایل</span></label></div><input id="customer-reply-gallery" type="file" accept="image/*" multiple data-gallery-input><input id="customer-reply-file" type="file" multiple data-document-input><span class="canonical-files">{{ form.files }}</span>{{ form.files.errors }}<small class="compression-note" data-compression-status>تصاویر بزرگ پیش از ارسال بهینه میشوند.</small></div>
|
||||
<div class="file-preview" data-file-preview></div>
|
||||
<div class="voice-recorder compact-recorder" data-voice-recorder hidden><div class="voice-head"><b>ضبط پیام صوتی</b><span class="badge" data-timer>00:00</span></div><div class="voice-actions"><button type="button" data-record>● شروع ضبط</button><button type="button" data-pause disabled>مکث</button><button type="button" data-stop disabled>توقف</button><button type="button" class="ghost" data-delete hidden>حذف</button></div><audio controls hidden></audio></div>
|
||||
<div class="composer-bar"><div class="composer-tools"><button class="tool-button" type="button" data-toggle-recorder title="افزودن توضیح صوتی">●<span>توضیح صوتی</span></button><button class="tool-button composer-close" type="button" data-composer-close>بستن</button></div><small class="composer-hint">پیش از ثبت، متن و پیوستها را بازبینی کنید.</small><button class="send-button" type="submit" data-primary-submit>ثبت پاسخ در تیکت <span>←</span></button></div>
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
<button type="button" class="composer-launch" data-composer-toggle aria-expanded="false"><span class="composer-launch-icon">+</span><span><b>ثبت اقدام جدید در تیکت</b><small>پاسخ رسمی یا یادداشت کارشناسی ثبت کنید</small></span><span class="composer-launch-arrow">↑</span></button>
|
||||
<div class="mode" role="tablist"><button type="button" role="tab" data-composer-mode="reply" @click="mode='reply'" :class="mode==='reply'&&'active'">پاسخ رسمی به مشتری</button>{% if perms.messaging.add_internal_note %}<button type="button" role="tab" data-composer-mode="note" @click="mode='note'" :class="mode==='note'&&'active note-button'">🔒 یادداشت کارشناسی</button>{% endif %}</div>
|
||||
<form method="post" enctype="multipart/form-data" data-ticket-composer data-chat-coach data-chat-coach-key="ticket-{{ ticket.public_id }}-staff-{{ user.public_id }}" :action="mode==='note'?'{% url 'staff_tickets:note' ticket.public_id %}':'{% url 'staff_tickets:reply' ticket.public_id %}'">{% csrf_token %}
|
||||
<div class="composer-context"><div><b x-text="mode==='note'?'ثبت یادداشت کارشناسی در پرونده':'ثبت پاسخ رسمی در تیکت'"></b><small x-text="mode==='note'?'این یادداشت فقط برای تیم کارشناسی قابل مشاهده است.':'پاسخ را کامل و یکجا بنویسید؛ این متن برای مشتری ارسال و اعلان میشود.'"></small></div><div class="composer-context-actions"><span data-draft-status></span><span class="composer-send-icon" role="button" tabindex="0" data-scroll-to-submit aria-label="رفتن به دکمه ثبت پاسخ" title="رفتن به دکمه ثبت پاسخ"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 20.5 21 12 3 3.5v6.6l12 1.9-12 1.9z"/></svg></span></div></div>
|
||||
{{ form.body }}{{ form.body.errors }}<div class="reply-media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="staff-reply-gallery">▣ انتخاب عکس از گالری</label><label class="media-picker-button" for="staff-reply-file">+ انتخاب فایل</label></div><input id="staff-reply-gallery" type="file" accept="image/*" multiple data-gallery-input><input id="staff-reply-file" type="file" multiple data-document-input><span class="canonical-files">{{ form.files }}</span>{{ form.files.errors }}<small class="compression-note" data-compression-status>تصاویر بزرگ پیش از ارسال بهینه میشوند.</small></div><div class="file-preview" data-file-preview></div>
|
||||
<div class="composer-context"><div><b x-text="mode==='note'?'ثبت یادداشت کارشناسی در پرونده':'ثبت پاسخ رسمی در تیکت'"></b><small x-text="mode==='note'?'این یادداشت فقط برای تیم کارشناسی قابل مشاهده است.':'پاسخ برای مشتری ارسال میشود و اعلان خواهد شد.'"></small></div></div>
|
||||
<div class="composer-input-row"><div class="composer-editor">{{ form.body }}{{ form.body.errors }}<span class="composer-draft-status" data-draft-status></span></div><span class="composer-send-icon" role="button" tabindex="0" data-scroll-to-submit aria-label="رفتن به دکمه ثبت پاسخ" title="رفتن به دکمه ثبت پاسخ"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3 20.5 21 12 3 3.5v6.6l12 1.9-12 1.9z"/></svg></span></div>
|
||||
<div class="reply-media-picker"><div class="media-picker-actions"><label class="media-picker-button gallery-button" for="staff-reply-gallery"><span class="media-picker-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M4 5h3l1.2-1.5h7.6L17 5h3a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2Zm8 3.2a4.4 4.4 0 1 0 0 8.8 4.4 4.4 0 0 0 0-8.8Z"/></svg></span><span>انتخاب عکس از گالری</span></label><label class="media-picker-button" for="staff-reply-file"><span class="media-picker-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M8.5 12.8 14.8 6.5a3 3 0 1 1 4.2 4.2l-8.5 8.5a5 5 0 0 1-7.1-7.1l8-8 1.4 1.4-8 8a3 3 0 1 0 4.3 4.3l8.5-8.5a1 1 0 0 0-1.4-1.4l-6.4 6.3a1 1 0 0 0 1.4 1.5l5.7-5.7 1.4 1.4-5.7 5.7a3 3 0 1 1-4.2-4.3Z"/></svg></span><span>انتخاب فایل</span></label></div><input id="staff-reply-gallery" type="file" accept="image/*" multiple data-gallery-input><input id="staff-reply-file" type="file" multiple data-document-input><span class="canonical-files">{{ form.files }}</span>{{ form.files.errors }}<small class="compression-note" data-compression-status>تصاویر بزرگ پیش از ارسال بهینه میشوند.</small></div><div class="file-preview" data-file-preview></div>
|
||||
<div class="composer-extra" hidden data-composer-extra>{{ form.mentioned_experts.label_tag }}{{ form.mentioned_experts }}</div>
|
||||
<div class="voice-recorder compact-recorder" data-voice-recorder hidden><div class="voice-head"><b>افزودن توضیح صوتی</b><span class="badge" data-timer>00:00</span></div><div class="voice-actions"><button type="button" data-record>● شروع ضبط</button><button type="button" data-pause disabled>مکث</button><button type="button" data-stop disabled>توقف</button><button type="button" class="ghost" data-delete hidden>حذف</button></div><audio controls hidden></audio></div>
|
||||
<div class="composer-bar"><div class="composer-tools"><button class="tool-button" type="button" data-toggle-recorder>●<span>توضیح صوتی</span></button><button class="tool-button" type="button" data-toggle-extra>@<span>اطلاع به کارشناس</span></button><button class="tool-button composer-close" type="button" data-composer-close>بستن</button></div><small class="composer-hint">پیش از ثبت، متن و پیوستها را بازبینی کنید.</small><button class="send-button" type="submit" data-primary-submit><span x-text="mode==='note'?'ثبت یادداشت در پرونده':'ثبت پاسخ در تیکت'"></span> ←</button></div>
|
||||
|
||||
@@ -53,6 +53,35 @@ def assert_reply_input_is_visible(page):
|
||||
assert metrics["visibleHeight"] >= min(64, metrics["inputHeight"] * 0.6)
|
||||
|
||||
|
||||
def assert_compact_composer_controls(page):
|
||||
metrics = page.evaluate(
|
||||
"""() => {
|
||||
const input = document.querySelector('form[data-ticket-composer] textarea').getBoundingClientRect();
|
||||
const jump = document.querySelector('[data-scroll-to-submit]').getBoundingClientRect();
|
||||
const pickers = [...document.querySelectorAll('.media-picker-button')].map(node => node.getBoundingClientRect());
|
||||
return {
|
||||
inputBottom: input.bottom,
|
||||
jumpBottom: jump.bottom,
|
||||
pickerTopDifference: Math.abs(pickers[0].top - pickers[1].top),
|
||||
pickersOverlap: !(pickers[0].right <= pickers[1].left || pickers[1].right <= pickers[0].left),
|
||||
};
|
||||
}"""
|
||||
)
|
||||
assert abs(metrics["inputBottom"] - metrics["jumpBottom"]) <= 8
|
||||
assert metrics["pickerTopDifference"] <= 2
|
||||
assert not metrics["pickersOverlap"]
|
||||
|
||||
|
||||
def assert_textarea_grows_with_content(page):
|
||||
reply = page.locator('form[data-ticket-composer] textarea')
|
||||
reply.fill("پاسخ کوتاه")
|
||||
compact_height = reply.bounding_box()["height"]
|
||||
reply.fill("خط اول\nخط دوم\nخط سوم\nخط چهارم\nخط پنجم\nخط ششم\nخط هفتم")
|
||||
grown_height = reply.bounding_box()["height"]
|
||||
assert compact_height <= 52
|
||||
assert compact_height < grown_height <= 145
|
||||
|
||||
|
||||
def test_magic_link_mobile_viewport_keeps_composer_messages_and_credit_visible(live_server, browser_page):
|
||||
branding = SiteBranding.load()
|
||||
branding.developer_credit_text = "امضای مقدس توسعهدهنده E2E"
|
||||
@@ -78,15 +107,17 @@ def test_magic_link_mobile_viewport_keeps_composer_messages_and_credit_visible(l
|
||||
conversation.expect_latest_message_above_composer()
|
||||
|
||||
conversation.open_composer()
|
||||
assert_compact_composer_controls(page)
|
||||
page.set_viewport_size({"width": 390, "height": 430})
|
||||
expect(page.locator('form[data-ticket-composer] textarea')).to_be_focused()
|
||||
expect(page.locator("[data-composer-shell]")).to_have_attribute("data-keyboard-open", "true")
|
||||
page.locator('form[data-ticket-composer] textarea').fill("متن پاسخ باید هنگام تایپ دیده شود")
|
||||
assert_textarea_grows_with_content(page)
|
||||
assert_reply_input_is_visible(page)
|
||||
expect(page.locator("[data-composer-close]")).to_be_hidden()
|
||||
expect(credit).to_be_visible()
|
||||
assert_workspace_fits_visual_viewport(page)
|
||||
|
||||
page.locator("[data-composer-close]").click()
|
||||
page.set_viewport_size({"width": 390, "height": 844})
|
||||
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
|
||||
expect(credit).to_be_visible()
|
||||
assert_workspace_fits_visual_viewport(page)
|
||||
@@ -116,7 +147,8 @@ def test_staff_ticket_mobile_viewport_survives_open_close_and_rotation(live_serv
|
||||
page.set_viewport_size({"width": 844, "height": 390})
|
||||
assert_workspace_fits_visual_viewport(page)
|
||||
expect(page.locator("[data-composer-shell]")).to_have_attribute("data-keyboard-open", "true")
|
||||
page.locator('form[data-ticket-composer] textarea').fill("پاسخ کارشناس هنگام باز بودن کیبورد")
|
||||
assert_compact_composer_controls(page)
|
||||
assert_textarea_grows_with_content(page)
|
||||
assert_reply_input_is_visible(page)
|
||||
expect(page.locator("[data-composer-close]")).to_be_visible()
|
||||
|
||||
|
||||
@@ -198,11 +198,12 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
||||
conversation.expect_latest_message_above_composer()
|
||||
launch.click()
|
||||
close_composer = page.get_by_role("button", name="بستن", exact=True)
|
||||
expect(close_composer).to_be_visible()
|
||||
close_box = close_composer.bounding_box()
|
||||
assert close_box is not None
|
||||
assert close_box["x"] >= 0 and close_box["x"] + close_box["width"] <= 390
|
||||
assert close_box["y"] >= 0 and close_box["y"] + close_box["height"] <= 844
|
||||
expect(close_composer).to_be_hidden()
|
||||
expect(page.locator('form[data-ticket-composer] textarea')).to_be_focused()
|
||||
page.set_viewport_size({"width":390,"height":430})
|
||||
expect(page.locator("[data-composer-shell]")).to_have_attribute("data-keyboard-open","true")
|
||||
page.set_viewport_size({"width":390,"height":844})
|
||||
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
|
||||
conversation.expect_latest_message_above_composer()
|
||||
dismiss_context.click()
|
||||
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)
|
||||
@@ -452,6 +452,7 @@ def test_ticket_workspace_uses_mobile_viewport_and_keyboard_safe_layout_contract
|
||||
ui_script=Path("static/js/ui.js").read_text(encoding="utf-8")
|
||||
conversation_script=Path("static/js/conversation.js").read_text(encoding="utf-8")
|
||||
magic_template=Path("templates/tickets/customer_detail.html").read_text(encoding="utf-8")
|
||||
staff_template=Path("templates/tickets/staff_detail.html").read_text(encoding="utf-8")
|
||||
assert "interactive-widget=resizes-content" in base
|
||||
for feature in ("100svh","--visual-viewport-height","overscroll-behavior:none","display-mode:standalone"):
|
||||
assert feature in source_css
|
||||
@@ -463,6 +464,16 @@ def test_ticket_workspace_uses_mobile_viewport_and_keyboard_safe_layout_contract
|
||||
assert "focus({preventScroll:true})" in conversation_script
|
||||
for feature in ("is-keyboard-active","ensureInputVisible","stableViewportHeight-height>120","data-keyboard-open"):
|
||||
assert feature in conversation_script or feature in source_css
|
||||
assert "input.setAttribute('rows','1')" in conversation_script
|
||||
assert "Math.min(Math.max(contentHeight,48),maxHeight)" in conversation_script
|
||||
assert "keyboardClosed" in conversation_script and "setComposerOpen(false,false)" in conversation_script
|
||||
assert "لطفاً توضیحات خود را کامل، دقیق و یکجا بنویسید" not in magic_template
|
||||
for template in (magic_template,staff_template):
|
||||
assert 'class="composer-input-row"' in template
|
||||
assert template.count('class="media-picker-icon"')==2
|
||||
assert 'data-toggle-recorder' in template and 'data-gallery-input' in template and 'data-document-input' in template
|
||||
for feature in (".composer-input-row","grid-template-columns:repeat(2,minmax(0,1fr))",".ticket-workspace .composer-close{display:none!important}"):
|
||||
assert feature in source_css
|
||||
assert '<footer class="ticket-detail-credit">{{ developer_credit }}</footer>' in magic_template
|
||||
|
||||
def test_frontend_runtime_libraries_are_local_and_versioned():
|
||||
|
||||
Reference in new issue
Block a user