Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scripts/smoke-web.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ const requiredStrings = [
{ name: 'JD triage modal', re: /id="jdtriage"/ },
{ name: 'Triage a JD control', re: /Triage a JD/ },
{ name: 'JD triage Add to board', re: /id="jt_add"/ },
{ name: 'modal focus trap helper', re: /export const createFocusTrap/ },
{ name: 'shared modal trap/close helpers', re: /function trapModal\(|function closeModal\(/ },
{ name: 'settings dialog name', re: /id="settings"[^>]*aria-labelledby="settings_heading"/ },
{ name: 'addrole dialog name', re: /id="addrole"[^>]*aria-labelledby="ar_heading"/ },
{ name: 'jdtriage dialog name', re: /id="jdtriage"[^>]*aria-labelledby="jt_heading"/ },
{ name: 'ligoogle dialog name', re: /id="ligoogle"[^>]*aria-labelledby="li_heading"/ },
{ name: 'rolepanel dialog name', re: /id="rolepanel"[^>]*aria-labelledby="rp_title"/ },
{ name: 'modals use aria-modal', re: /id="settings"[^>]*aria-modal="true"/ },
{ name: 'no legacy Resume tool button', re: />📄 Resume tool</, invert: true },
{ name: 'no standalone Chat button', re: /id="chatbtn"/, invert: true },
{ name: 'no standalone chat modal', re: /Job-search chat/, invert: true },
Expand Down
18 changes: 9 additions & 9 deletions web/index.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 69 additions & 0 deletions web/ui/primitives.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,74 @@
export const byId = (id) => document.getElementById(id)

const FOCUSABLE_SEL = [
'button:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'textarea:not([disabled])',
'select:not([disabled])',
'a[href]',
'[tabindex]:not([tabindex="-1"])',
].join(', ')

function isVisibleFocusable(el, root) {
if (!el || el.disabled || el.getAttribute('aria-hidden') === 'true') return false
if (el.tabIndex < 0 && !el.matches('a[href], button, input, textarea, select')) return false
// Skip nodes inside a nested .hidden / [hidden] region (not the modal root itself).
let node = el
while (node && node !== root) {
if (node.hidden || node.classList?.contains('hidden')) return false
node = node.parentElement
}
const style = window.getComputedStyle(el)
return style.visibility !== 'hidden' && style.display !== 'none'
}

/** Trap Tab inside a .modal (prefer .box). Returns a release fn that restores focus to opener. */
export const createFocusTrap = (modalEl, openerEl, initialEl) => {
if (!modalEl) return () => {}
const root = modalEl.querySelector('.box') || modalEl

const focusables = () => [...root.querySelectorAll(FOCUSABLE_SEL)]
.filter((el) => isVisibleFocusable(el, root))

const start = initialEl && root.contains(initialEl) && isVisibleFocusable(initialEl, root)
? initialEl
: focusables()[0]
if (start) start.focus()
else if (!root.hasAttribute('tabindex')) {
root.setAttribute('tabindex', '-1')
root.focus()
} else root.focus()

function handleKeydown(e) {
if (e.key !== 'Tab') return
const list = focusables()
if (!list.length) {
e.preventDefault()
root.focus()
return
}
const first = list[0]
const last = list[list.length - 1]
const active = document.activeElement
if (e.shiftKey && (active === first || !root.contains(active))) {
e.preventDefault()
last.focus()
} else if (!e.shiftKey && (active === last || !root.contains(active))) {
e.preventDefault()
first.focus()
}
}

modalEl.addEventListener('keydown', handleKeydown)

return function releaseFocusTrap() {
modalEl.removeEventListener('keydown', handleKeydown)
if (openerEl && typeof openerEl.focus === 'function') {
try { openerEl.focus() } catch (_e) { /* opener may be gone */ }
}
}
}

export const escapeHtml = (value) => String(value || '').replace(
/[&<>"]/g,
(character) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[character],
Expand Down
63 changes: 45 additions & 18 deletions web/ui/state.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ import {
import {
buildTriageRoleRow, buildMatchReportRow, splitGapsByMaterials, validateTriageAdd, inferRoleLevel,
} from './board.mjs'
import { byId, escapeHtml, commaList } from './primitives.mjs'
import { byId, escapeHtml, commaList, createFocusTrap } from './primitives.mjs'
import {
createOpenaiPrefsStore,
providerSecretOnFile,
Expand All @@ -116,6 +116,19 @@ const $ = byId
const esc = escapeHtml
const list = commaList
let SELF_HOST_SETUP_ERROR = ''
/** Per-modal focus-trap release fns (settings / addrole / jdtriage / ligoogle / rolepanel). */
const modalRelease = Object.create(null)
function trapModal(id, opener, focusEl){
modalRelease[id]?.()
const el=$(id); if(!el) return
el.classList.remove('hidden')
modalRelease[id]=createFocusTrap(el, opener||document.activeElement, focusEl||null)
}
function closeModal(id){
$(id)?.classList.add('hidden')
modalRelease[id]?.()
modalRelease[id]=null
}

function selfHostSetupError(error){
const code = String(error?.code || '')
Expand Down Expand Up @@ -1583,18 +1596,20 @@ board.addEventListener('click',e=>{ if(suppressClick) return; if(e.target.closes

// ---- role panel: JD auto-load → scan / tailor / cover / jobscan / download / apply ----
let CURROLE=null, RPKIND='resume'
$('rp_close').onclick=()=> $('rolepanel').classList.add('hidden')
$('rp_close').onclick=()=> closeModal('rolepanel')
async function openRole(id){
if(APP_SECTION!=='board') showAppSection('board')
if(NEWPANEL) return openRole2(id)
const opener = document.activeElement
CURROLE=findRole(id); if(!CURROLE) return
id=roleIdOf(CURROLE.id)
logEvent('role_open', id)
$('rp_title').textContent=CURROLE.title; $('rp_co').textContent=(CURROLE.company||'')+(CURROLE.match_score?' · '+CURROLE.match_score+' match':'')
$('rp_apply').style.display = CURROLE.url ? '' : 'none'; if(CURROLE.url) $('rp_apply').href=CURROLE.url
$('rp_err').textContent=''; $('rp_matchout').classList.add('hidden'); $('rp_out').classList.add('hidden')
$('rp_jd').value=''; $('rp_jobscan').value=''; $('rp_jdstate').textContent='· loading job description…'
$('rolepanel').classList.remove('hidden')
trapModal('rolepanel', opener, $('rp_jd'))

loadSaved(id)
if(CURROLE.jd){
if(jdRejectReason(CURROLE.jd)){
Expand Down Expand Up @@ -3372,7 +3387,10 @@ function rp2RenderPick(){
rp2GenNote()
const rs=wrap.querySelector('#rp2_resplit'); if(rs) rs.onclick=()=>rp2Resplit()
const os=wrap.querySelector('#rp2_opensettings'); if(os) os.onclick=()=>{
closeDrawer(); closeBuilder(); $('settings').classList.remove('hidden'); const ta=$('s_resume'); if(ta){ ta.focus(); ta.scrollIntoView({block:'center'}) }
closeDrawer(); closeBuilder()
const ta=$('s_resume')
trapModal('settings', os, ta)
ta?.scrollIntoView({block:'center'})
}
wrap.querySelectorAll('input[data-bid]').forEach(cb=>cb.onchange=()=>{ const bid=cb.dataset.bid
if(cb.checked){ if(!RP2SEL.bullet_ids.includes(bid)) RP2SEL.bullet_ids.push(bid) }
Expand Down Expand Up @@ -3970,16 +3988,18 @@ function guessCompanyFromHost(url){
return h.split('.')[0].replace(/-/g,' ')
}catch(_e){ return 'Unknown' }
}

function openLiGoogleModal(){
const { q }=liSearchBits()
if($('li_query_preview')) $('li_query_preview').textContent='Google query: site:linkedin.com/jobs/view '+q
if($('li_urls')) $('li_urls').value=''
if($('li_jd_bulk')) $('li_jd_bulk').value=''
if($('li_err')) $('li_err').textContent=''
$('ligoogle')?.classList.remove('hidden')
trapModal('ligoogle', $('li_google_btn'))
}
$('li_google_btn')&&($('li_google_btn').onclick=()=>openLiGoogleModal())
$('li_close')&&($('li_close').onclick=()=>$('ligoogle').classList.add('hidden'))
$('li_close')&&($('li_close').onclick=()=> closeModal('ligoogle'))

$('li_open_google')&&($('li_open_google').onclick=()=>window.open(liGoogleUrl(),'_blank','noopener'))
$('li_open_native')&&($('li_open_native').onclick=()=>window.open(liNativeUrl(),'_blank','noopener'))
$('li_import')&&($('li_import').onclick=async()=>{
Expand Down Expand Up @@ -4019,7 +4039,7 @@ $('li_import')&&($('li_import').onclick=async()=>{
}catch(_e){}
}
}
$('ligoogle').classList.add('hidden')
closeModal('ligoogle')
await load()
$('status').textContent = added
? `Added ${added} role${added>1?'s':''} from your links${skipped?` (${skipped} duplicates)`:''}${blocked?` · ${blocked} blocklisted`:''}. Edit titles/companies and paste JDs where needed.`
Expand All @@ -4031,9 +4051,10 @@ $('li_import')&&($('li_import').onclick=async()=>{
$('addrolebtn').onclick=()=>{
$('ar_company').value=''; $('ar_title').value=''; $('ar_url').value=''
if($('ar_jd')) $('ar_jd').value=''
$('ar_err').textContent=''; $('addrole').classList.remove('hidden'); $('ar_company').focus()
$('ar_err').textContent=''
trapModal('addrole', $('addrolebtn'), $('ar_company'))
}
$('ar_close').onclick=()=> $('addrole').classList.add('hidden')
$('ar_close').onclick=()=> closeModal('addrole')
/** Shared Add-role insert — blocklist, dedupe, JD reject/format, ghost risk. */
async function insertManualRoleOnBoard({ company, title, url, jd, stage }){
if(!company||!title) return { error:'Company and job title are required.' }
Expand Down Expand Up @@ -4066,7 +4087,7 @@ $('ar_save').onclick=async()=>{
try{
const out=await insertManualRoleOnBoard({ company, title, url, jd, stage:'researched' })
if(out.error){ $('ar_err').textContent=out.error; return }
$('addrole').classList.add('hidden')
closeModal('addrole')
await load()
if(out.data?.id) openRole(out.data.id)
}catch(e){ $('ar_err').textContent=e.message||String(e) }
Expand Down Expand Up @@ -4095,16 +4116,17 @@ function jtSyncAddEnabled(){
;['jt_company','jt_title','jd'].forEach(id=>{
const el=$(id); if(el) el.addEventListener('input', jtSyncAddEnabled)
})

$('jdtriagebtn').onclick=()=>{
TRIAGE_LAST_MATCH=null
$('jdtriage').classList.remove('hidden')
$('matchout').classList.add('hidden')
$('resumeerr').textContent=''
if($('jt_gaps')) $('jt_gaps').innerHTML=''
jtSyncAddEnabled()
$('jd')?.focus()
trapModal('jdtriage', $('jdtriagebtn'), $('jd') || $('jt_company'))
}
$('jdtriageclose').onclick=()=> $('jdtriage').classList.add('hidden')
$('jdtriageclose').onclick=()=> closeModal('jdtriage')

$('matchbtn').onclick = async ()=>{
$('resumeerr').textContent=''
const jd=($('jd')?.value||'').trim()
Expand Down Expand Up @@ -4159,7 +4181,7 @@ $('jt_add').onclick=async()=>{
}
logEvent('jd_triage_add', role?.id, { scored: !!TRIAGE_LAST_MATCH })
const scoredLabel = TRIAGE_LAST_MATCH?.match_score!=null ? ` · match ${TRIAGE_LAST_MATCH.match_score}%` : ''
$('jdtriage').classList.add('hidden')
closeModal('jdtriage')
TRIAGE_LAST_MATCH=null
await load()
if(role?.id) openRole(role.id)
Expand All @@ -4176,6 +4198,7 @@ function paintSettingsSecrets(){
if(ks) ks.innerHTML = painted.keysHtml
if(hs) hs.innerHTML = painted.humanHtml
}

$('settingsbtn').onclick=()=>{
FIND_PREFS = loadFindPrefs()
$('s_titles').value=(PROFILE?.target_titles||[]).join(', ')
Expand Down Expand Up @@ -4218,9 +4241,10 @@ $('settingsbtn').onclick=()=>{
? 'Titles look Director/VP-weighted but Seniority is blank — Find treats blank as any level. Add “director, vp” here if you want that gate.'
: (FIND_PREFS.max_age_days===0 ? 'Max age 0 = no age limit. Soft-hide only affects Sourced.' : '')
}
$('settings').classList.remove('hidden')
trapModal('settings', $('settingsbtn'), $('s_titles'))
}
$('settingsclose').onclick=()=> $('settings').classList.add('hidden')

$('settingsclose').onclick=()=> closeModal('settings')
$('exp_json').onclick = async ()=>{
const [{data:prof},{data:roles},{data:reports}] = await Promise.all([
sb.from('mt_profiles').select('*').eq('owner',ME.id),
Expand Down Expand Up @@ -4496,7 +4520,7 @@ $('s_save').onclick = async ()=>{
$('seterr').style.color='#1a7f37'
$('seterr').textContent='Saved. Keys stay hidden — status above shows “key on file”. Find filters applied.'
load()
setTimeout(()=>{ $('seterr').style.color=''; $('seterr').textContent=''; $('settings').classList.add('hidden') }, 1200)
setTimeout(()=>{ $('seterr').style.color=''; $('seterr').textContent=''; closeModal('settings') }, 1200)
}

// ==== Hybrid IA chrome: drawer verdict, tailor CTA, builder sync, Esc ====
Expand Down Expand Up @@ -4568,7 +4592,10 @@ document.addEventListener('keydown', e=>{
if(!$('builderView')?.classList.contains('hidden')){ closeBuilder(); return }
if(!$('drawer')?.classList.contains('hidden')){
if(!$('rp2_jdwrap')?.classList.contains('hidden')) return // keep paste session
rp2FlushSel(); closeDrawer()
rp2FlushSel(); closeDrawer(); return
}
for(const id of ['settings','jdtriage','addrole','ligoogle','rolepanel']){
if(!$(id)?.classList.contains('hidden')){ closeModal(id); return }
}
})
$('dw_verdict')?.addEventListener('click', e=>{
Expand Down
Loading