From fff171cce479dfe14f307a22c6e9743545726713 Mon Sep 17 00:00:00 2001 From: soulhackzlol Date: Tue, 13 Oct 2026 01:06:15 +0200 Subject: [PATCH] fix(integrations): Your values lists by integration and stays put while typing A value's group and field were decided again on every key, so a row jumped to another group, or swapped its field, under the typing. And any text over 60 characters was filed under "Lists a step picks from". Values are now listed under the integration that reads them, in groups that fold: more than eight values open as the list of integrations. A row's group and field are read once, when the window opens. A long single text gets a roomy box of its own instead of the list editor. --- CHANGELOG.md | 12 ++++--- web/index.html | 12 +++++-- web/integrations.js | 82 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 81 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19373c2..f02b3bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -323,10 +323,14 @@ What a chat economy needs, as steps you can build anything else with too. - **Your values** (a button on the Integrations tab): things you set once and any message or step reads as `{my.name}`, like what a point is called, how often a game is won or a list to pick from. Change one there - and everything that uses it follows. Each value shows what it is for, - where it is used, and one field made for it: a slider for a chance, a - box with one entry per line for a list. Its name and label sit behind - the pencil. A second tab shows what your viewers hold, where you can set + and everything that uses it follows. Values are listed under the + integration that reads them ("Spike Roulette", "Chat Coins: wallet"), and + with more than a few the window opens as that list of integrations, each + one click away, with a box to find a value by name. Each value shows what + it is for and one field made for it: a slider for a chance, a box with + one entry per line for a list, a roomy box for a sentence. Nothing moves + while you type. Its name and label sit behind the pencil. A second tab + shows what your viewers hold, where you can set one viewer's amount or start everyone over. In a counting word, write a value with a `$`: `{viewer.coins#$my.coin#$my.coins}` says "1 gem" or "5 gems" in your words. A recipe brings the values it uses; one you diff --git a/web/index.html b/web/index.html index 2b66f94..1304bfa 100644 --- a/web/index.html +++ b/web/index.html @@ -3344,8 +3344,16 @@ .ig-val-body{gap:0} .ig-val-tabs{margin-bottom:14px} .ig-val-find{margin-bottom:6px} -.ig-val-group{margin-top:14px} -.ig-val-group > .ic-label{margin-bottom:2px} +.ig-val-group{margin-top:6px} +.ig-val-head{display:flex;align-items:center;gap:8px;width:100%;padding:9px 10px;border:0;border-radius:10px;background:var(--bg-2);color:var(--fg);font:inherit;font-size:13px;font-weight:600;text-align:left;cursor:pointer} +.ig-val-head:hover,.ig-val-head:focus-visible{background:var(--surface-3);outline:0} +.ig-val-head svg{width:14px;height:14px;flex:none;color:var(--fg-4);transform:rotate(-90deg);transition:transform .25s var(--ease-out)} +.ig-val-group.open .ig-val-head svg{transform:none} +.ig-val-head-t{flex:1 1 0;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.ig-val-head-n{flex:none;font-size:12px;font-weight:400;color:var(--fg-3)} +.ig-val-group.open{margin-bottom:14px} +.ig-val-group.open .ig-val{padding:0 10px} +@media (prefers-reduced-motion: reduce){.ig-val-head svg{transition:none}} .ig-val{border-bottom:1px solid var(--line)} .ig-val-main{display:flex;align-items:center;gap:12px;padding:10px 0;min-height:56px} .ig-val.wide .ig-val-main{flex-wrap:wrap;align-items:flex-start} diff --git a/web/integrations.js b/web/integrations.js index 49c86ad..efc1ada 100644 --- a/web/integrations.js +++ b/web/integrations.js @@ -7023,14 +7023,25 @@ function recipeWarningHtml(p){ // Your values: things set once ({my.name}) that any message or step reads, // so a coin's name or a game's odds change in one place; and, on its own -// tab, what viewers hold. Each row shows what the value is for and one -// field to change it; its name and label hide behind the pencil, since -// renaming is rare and changing the value is the point. +// tab, what viewers hold. Values are listed under the integration that +// reads them. Each row shows what the value is for and one field to change +// it; its name and label hide behind the pencil, since renaming is rare and +// changing the value is the point. +// Where a value added in this window sits until it is saved. "+" can't be +// an integration's id. +const NEW_VALUE_HOME = {id:'+', title:'New', others:0}; +const VALUES_SHOWN_AT_ONCE = 8; const Values = { - list:[], tab:'values', find:'', more:null, saved:'', + list:[], tab:'values', find:'', more:null, saved:'', unfolded:new Set(), open(){ - this.list = clone(S.data.values || []); + // A row's kind and home are read once, here: decided again on every + // key, a row would jump to another group, or swap its field, under the + // typing. + this.list = clone(S.data.values || []).map(v => ({...v, kind:this.kindOf(v), home:this.homeOf(v.name)})); this.saved = JSON.stringify(this.clean()); + // A few values show at once. Many open as a list of their integrations, + // each one click away: thirty rows in a row is a wall to scroll. + this.unfolded = new Set(this.list.length <= VALUES_SHOWN_AT_ONCE ? this.list.map(v => v.home.id) : []); this.tab = 'values'; this.find = ''; this.more = null; @@ -7047,19 +7058,30 @@ const Values = { dirty(){ return JSON.stringify(this.clean()) !== this.saved; }, // How a value is best edited, read off what it holds and what it is for. kindOf(v){ - if (/\n/.test(v.value) || v.value.length > 60 || / \| /.test(v.value)) return 'list'; + if (/\n/.test(v.value) || / \| /.test(v.value)) return 'list'; + // A sentence, like what a command answers: room to read it, but no list. + if (v.value.length > 60) return 'long'; if (!/^-?\d+(\.\d+)?$/.test(v.value.trim())) return 'text'; return /chance|percent|%/i.test(v.name + ' ' + v.label) ? 'chance' : 'number'; }, - // How many integrations read {my.} somewhere. - usedIn(name){ - if (!name) return 0; + // The integrations that read {my.} somewhere. + readers(name){ + if (!name) return []; const reads = new RegExp(`[{$]my\\.${name}(?![A-Za-z0-9_])`); - return S.data.integrations.filter(i => reads.test(JSON.stringify(i))).length; + return S.data.integrations.filter(i => reads.test(JSON.stringify(i))); + }, + usedIn(name){ return this.readers(name).length; }, + // Where a value is listed: under the first integration that reads it, + // which is what a streamer looks for ("the coins game"), not under what + // sort of text it holds. `others`: how many more read it too. + homeOf(name){ + const [first, ...rest] = this.readers(name); + return first ? {id:first.id, title:first.name, others:rest.length} : {id:'', title:'Not used yet', others:0}; }, editorHtml(v, n){ const at = `data-n="${n}"`; - switch (this.kindOf(v)){ + switch (v.kind){ + case 'long': return ``; case 'chance': return ` %`; case 'number': return ``; @@ -7069,12 +7091,12 @@ const Values = {
${/\n/.test(v.value) ? `${plural(lines, 'line')}: one is picked each time.` : 'One per line, or separated by |.'}
`; } } - return ``; + return ``; }, rowHtml(v, n){ - const kind = this.kindOf(v), open = this.more === n, used = this.usedIn(v.name.trim()); - const meta = !v.name ? 'Give it a name to use it' : `{my.${esc(v.name)}} · ${used ? 'used in ' + plural(used, 'integration') : 'not used yet'}`; - return `
+ const wide = ['list', 'long'].includes(v.kind), open = this.more === n, used = this.usedIn(v.name.trim()); + const meta = !v.name ? 'Give it a name to use it' : `{my.${esc(v.name)}}${v.home.others ? ' · also in ' + plural(v.home.others, 'other integration') : ''}`; + return `
${esc(v.label || v.name || 'New value')}${meta}
${this.editorHtml(v, n)}
@@ -7086,10 +7108,21 @@ const Values = { valuesHtml(){ const find = this.find.trim().toLowerCase(); const shown = this.list.map((v, n) => ({v, n})).filter(({v, n}) => n === this.more || !find || [v.name, v.label, v.value].some(x => x.toLowerCase().includes(find))); - const groups = [['text', 'Words'], ['number', 'Amounts'], ['chance', 'Chances'], ['list', 'Lists a step picks from']] - .map(([kind, title]) => ({title, rows:shown.filter(({v}) => this.kindOf(v) === kind)})).filter(g => g.rows.length); + // One group per integration, in the order of the list behind this + // window; then what nothing reads yet, then what was just added. Inside + // a group short fields come first and lists last. + const kinds = ['text', 'long', 'number', 'chance', 'list']; + const groups = S.data.integrations.map(i => i.id).concat('', NEW_VALUE_HOME.id) + .map(id => shown.filter(({v}) => v.home.id === id)).filter(rows => rows.length) + .map(rows => ({home:rows[0].v.home, rows:rows.sort((a, b) => kinds.indexOf(a.v.kind) - kinds.indexOf(b.v.kind) || a.n - b.n)})); return `${this.list.length > 6 ? `` : ''} - ${groups.map(g => `
${g.title}
${g.rows.map(({v, n}) => this.rowHtml(v, n)).join('')}
`).join('')} + ${groups.map(g => { + // What is being looked for, or being renamed, is never folded away. + const open = !!find || this.unfolded.has(g.home.id) || g.rows.some(({n}) => n === this.more); + return `
+ + ${open ? g.rows.map(({v, n}) => this.rowHtml(v, n)).join('') : ''}
`; + }).join('')} ${!this.list.length ? '
Nothing yet. A recipe you import brings its own values; or add one and write it as {my.name} wherever a value fits.
' : !shown.length ? '
Nothing matches.
' : ''} @@ -7140,9 +7173,20 @@ const Values = { const n = +el.dataset.n; if (a === 'v-tab'){ this.tab = el.dataset.v; this.render(); return true; } if (a === 'v-more'){ this.more = this.more === n ? null : n; this.render(); return true; } + if (a === 'v-group'){ + const id = el.dataset.v, folding = el.getAttribute('aria-expanded') === 'true'; + if (folding){ + this.unfolded.delete(id); + // Its open pencil would hold the group open. + if (this.more !== null && this.list[this.more].home.id === id) this.more = null; + } else this.unfolded.add(id); + this.render(); + return true; + } if (a === 'v-add'){ - this.list.push({name:'', label:'', value:''}); + this.list.push({name:'', label:'', value:'', kind:'text', home:NEW_VALUE_HOME}); this.more = this.list.length - 1; + this.unfolded.add(NEW_VALUE_HOME.id); this.find = ''; this.render(); focusField('v-label');