diff --git a/src/webpage/channel.ts b/src/webpage/channel.ts index 64e58ac..128652f 100644 --- a/src/webpage/channel.ts +++ b/src/webpage/channel.ts @@ -32,6 +32,7 @@ import {CustomHTMLDivElement} from "./index.js"; import {Direct} from "./direct.js"; import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js"; import {NotificationHandler} from "./notificationHandler.js"; +import {Command} from "./interactions/commands.js"; class Channel extends SnowFlake { editing!: Message | null; @@ -1578,6 +1579,38 @@ class Channel extends SnowFlake { this.textSave = MarkDown.gatherBoxText(typebox); typebox.textContent = ""; } + curCommand?: Command; + curWatch = () => {}; + async submitCommand() { + if (!this.curCommand) return; + if (await this.curCommand.submit(this)) { + this.curCommand = undefined; + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + typebox.markdown.boxEnabled = true; + typebox.innerHTML = ""; + typebox.markdown.boxupdate(); + typebox.removeEventListener("keyup", this.curWatch); + } + } + startCommand(command: Command) { + this.curCommand = command; + const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + typebox.markdown.boxEnabled = false; + const func = () => { + const node = window.getSelection()?.focusNode; + if (this.localuser.channelfocus === this) { + const out = command.collect(typebox, this, node || undefined); + if (!out) { + typebox.markdown.boxEnabled = true; + typebox.markdown.boxupdate(); + typebox.removeEventListener("keyup", func); + } + } + }; + this.curWatch = func; + typebox.addEventListener("keyup", func); + command.render(typebox, this); + } async getHTML(addstate = true, getMessages: boolean | void = undefined, aroundMessage?: string) { if (this.owner instanceof Direct) { this.owner.freindDiv?.classList.remove("viewChannel"); @@ -1586,11 +1619,17 @@ class Channel extends SnowFlake { this.localuser.channelfocus.collectBox(); } const typebox = document.getElementById("typebox") as CustomHTMLDivElement; + typebox.markdown.boxEnabled = !this.curCommand; + if (this.curCommand) { + this.curCommand.render(typebox, this); + } typebox.style.setProperty("--channel-text", JSON.stringify(I18n.channel.typebox(this.name))); - const md = typebox.markdown; - md.owner = this; - typebox.textContent = this.textSave; - md.boxupdate(Infinity); + if (!this.curCommand) { + const md = typebox.markdown; + md.owner = this; + typebox.textContent = this.textSave; + md.boxupdate(Infinity); + } this.localuser.fileExtange(this.files, this.htmls); if (getMessages === undefined) { @@ -1843,7 +1882,7 @@ class Channel extends SnowFlake { headers: this.headers, }); - const response = await j.json(); + const response = (await j.json()) as messagejson[]; if (response.length !== 100) { this.allthewayup = true; } @@ -1859,6 +1898,10 @@ class Channel extends SnowFlake { } prev = message; } + if (!response.length) { + this.lastmessageid = undefined; + this.lastreadmessageid = undefined; + } } delChannel(json: channeljson) { const build: Channel[] = []; @@ -1872,13 +1915,16 @@ class Channel extends SnowFlake { afterProm?: Promise; afterProms = new Map void>(); async grabAfter(id: string) { + if (this.idToNext.has(id)) { + return; + } if (id === this.lastmessage?.id) { return; } - if (this.afterProm) return this.afterProm; + if (this.afterProm) return new Promise((res) => this.afterProms.set(id, res)); let tempy: string | undefined = id; while (tempy && tempy.includes("fake")) { - tempy = this.idToNext.get(tempy); + tempy = this.idToPrev.get(tempy); } if (!tempy) return; id = tempy; diff --git a/src/webpage/guild.ts b/src/webpage/guild.ts index 2e318f0..f9e3131 100644 --- a/src/webpage/guild.ts +++ b/src/webpage/guild.ts @@ -18,6 +18,8 @@ import { templateSkim, mute_config, GuildOverrides, + commandJson, + applicationJson, } from "./jsontypes.js"; import {User} from "./user.js"; import {I18n} from "./i18n.js"; @@ -27,6 +29,7 @@ import {createImg} from "./utils/utils.js"; import {Sticker} from "./sticker.js"; import {ProgessiveDecodeJSON} from "./utils/progessiveLoad.js"; import {MarkDown} from "./markdown.js"; +import {Command} from "./interactions/commands.js"; export async function makeInviteMenu(inviteMenu: Options, guild: Guild, url: string) { const invDiv = document.createElement("div"); const bansp = ProgessiveDecodeJSON(url, { @@ -1289,7 +1292,7 @@ class Guild extends SnowFlake { } this.prevchannel = this.localuser.channelids.get(this.perminfo.prevchannel); this.stickers = json.stickers.map((_) => new Sticker(_, this)) || []; - //this.getCommands(); + this.getCommands(); } get perminfo() { return this.localuser.perminfo.guilds[this.id]; @@ -1849,14 +1852,56 @@ class Guild extends SnowFlake { }), }); } - + commands?: Command[]; + commandProm?: Promise; + apps?: applicationJson[]; + async getApps() { + if (this.commandProm) { + await this.commandProm; + } + if (this.apps) { + return this.commands; + } else { + const prom = this.getCommandsFetch(); + this.commandProm = prom; + const {apps, commands} = await prom; + this.commands = commands; + this.apps = apps; + return apps; + } + } async getCommands() { - const json = await ( + if (this.commandProm) { + await this.commandProm; + } + if (this.commands) { + return this.commands; + } else { + const prom = this.getCommandsFetch(); + this.commandProm = prom; + const {apps, commands} = await prom; + this.commands = commands; + this.apps = apps; + return commands; + } + } + + async getCommandsFetch() { + const json = (await ( await fetch(this.info.api + `/guilds/${this.id}/application-command-index`, { headers: this.headers, }) - ).json(); - if (this.id === "1006649183970562092") console.warn(json.application_commands); + ).json()) as {application_commands: commandJson[]; applications: applicationJson[]}; + //TODO remove this fix once the server fixes this + json.applications.forEach((_) => { + if (_.icon && _.icon.startsWith("data")) { + _.icon = null; + } + }); + return { + apps: json.applications, + commands: json.application_commands.map((_) => new Command(_, this.localuser)), + }; } } Guild.setupcontextmenu(); diff --git a/src/webpage/index.ts b/src/webpage/index.ts index f18dc4a..425dd38 100644 --- a/src/webpage/index.ts +++ b/src/webpage/index.ts @@ -154,6 +154,10 @@ async function handleEnter(event: KeyboardEvent): Promise { channel.typingstart(); if (event.key === "Enter" && !event.shiftKey) { + if (channel.curCommand) { + channel.submitCommand(); + return; + } event.preventDefault(); replyingTo = thisUser.channelfocus ? thisUser.channelfocus.replyingto : null; if (replyingTo?.div) { diff --git a/src/webpage/interactions/commands.ts b/src/webpage/interactions/commands.ts new file mode 100644 index 0000000..3aa1822 --- /dev/null +++ b/src/webpage/interactions/commands.ts @@ -0,0 +1,401 @@ +import {Channel} from "../channel.js"; +import {Guild} from "../guild.js"; +import {I18n} from "../i18n.js"; +import {commandJson, commandOptionJson} from "../jsontypes.js"; +import {Localuser} from "../localuser.js"; +import {SnowFlake} from "../snowflake.js"; +function focusInput(html: HTMLElement) { + const input = html.getElementsByTagName("input")[0]; + if (input) input.focus(); +} +function focusElm(node: HTMLElement | Text, before = true) { + const selection = window.getSelection(); + if (!selection) return; + var range = document.createRange(); + if (before) { + range.setStartBefore(node); + } else { + range.setStartAfter(node); + } + range.collapse(true); + selection.removeAllRanges(); + selection.addRange(range); +} +export class Command extends SnowFlake { + owner: Localuser | Guild; + type: 1 | 2 | 3 | 4; + applicationId: string; + name: string; + nameLocalizations: Record; + descriptionLocalizations: Record; + description: string; + defaultMemberPerms: BigInt; + permissions: { + user: boolean; + roles: Record; + channels: Record; + }; + nsfw: boolean; + gpr: number; + version: string; + handler: 1 | 2 | 3; + options: Option[]; + readonly rawJson: Readonly; + get localuser() { + if (this.owner instanceof Localuser) { + return this.owner; + } else { + return this.owner.owner; + } + } + constructor(command: commandJson, owner: Localuser | Guild) { + super(command.id); + this.rawJson = Object.freeze(structuredClone(command)); + this.owner = owner; + this.type = command.type; + this.applicationId = command.application_id; + this.name = command.name; + this.nameLocalizations = command.name_localizations || {}; + this.description = command.description; + this.descriptionLocalizations = command.description_localizations || {}; + this.defaultMemberPerms = BigInt(command.default_member_permissions || "0"); + this.permissions = { + user: command.permissions?.user || true, + roles: command.permissions?.roles || {}, + channels: command.permissions?.channels || {}, + }; + command.options ||= []; + this.options = command.options.map((_) => Option.toOption(_, this)); + this.nsfw = command.nsfw; + this.gpr = command.global_popularity_rank || 0; + this.version = command.version; + this.handler = command.handler || 1; + } + get localizedName() { + return this.nameLocalizations[I18n.lang] || this.name; + } + get localizedDescription() { + return this.descriptionLocalizations[I18n.lang] || this.description; + } + similar(search: string) { + if (search.length === 0) { + return 0.1; + } + const similar = (str: string) => { + if (str.includes(search)) { + return search.length / str.length; + } else if (str.toLowerCase().includes(search.toLowerCase())) { + return str.length / str.length / 1.4; + } else { + return 0; + } + }; + return Math.max( + similar(this.name), + similar(this.description), + similar(this.localizedDescription), + similar(this.localizedName), + ); + } + state = new WeakMap< + Channel, + ( + | { + option: Option; + state: string; + } + | string + )[] + >(); + collect(html: HTMLElement, channel: Channel, node?: Node): boolean { + const states = this.state.get(channel); + const build: ( + | { + option: Option; + state: string; + } + | string + )[] = []; + if (!states) return false; + + let gotname = false; + for (const elm of Array.from(html.childNodes)) { + if (elm instanceof HTMLElement) { + if (elm.classList.contains("commandFront")) { + gotname = true; + continue; + } + const name = elm.getAttribute("commandName"); + const state = states.find((_) => _ instanceof Object && _.option.match(name || "")); + if (state) { + build.push(state); + } else { + const option = this.options.find((_) => _.match(name || "")); + if (option) { + build.push({option, state: ""}); + } + } + } else if (elm instanceof Text) { + build.push(elm.textContent || ""); + } + } + + if (node instanceof Text) { + this.searchAtr(node, channel, html); + } + + if (gotname) { + this.state.set(channel, build); + } else { + this.state.delete(channel); + } + + return gotname; + } + searchAtr(textNode: Text, channel: Channel, Divhtml: HTMLElement) { + const text = (textNode.textContent || "").trim(); + const states = this.state.get(channel); + if (!states) { + this.localuser.MDSearchOptions( + [], + "", + document.getElementById("searchOptions") as HTMLDivElement, + ); + return; + } + const opts = this.options + .filter((obj) => !states.find((_) => _ instanceof Object && _.option === obj)) + .map((opt) => [opt, opt.similar(text)] as const) + .filter((_) => _[1]) + .sort((a, b) => a[1] - b[1]) + .slice(0, 6) + .map((_) => _[0]); + this.localuser.MDSearchOptions( + opts.map((opt) => { + return [ + opt.localizedName, + "", + void 0, + () => { + const html = opt.toHTML("", channel); + textNode.after(html); + textNode.remove(); + this.collect(Divhtml, channel); + console.log(this.state.get(channel)); + focusInput(html); + return true; + }, + ]; + }), + "", + document.getElementById("searchOptions") as HTMLDivElement, + ); + } + render(html: HTMLElement, channel: Channel) { + html.innerHTML = ""; + let state = this.state.get(channel); + if (!state) { + const req = this.options.filter((_) => _.required); + state = req.map((option) => ({option, state: ""})); + this.state.set(channel, state); + } + const command = document.createElement("span"); + command.classList.add("commandFront"); + command.textContent = `/${this.localizedName}`; + command.contentEditable = "false"; + html.append(command); + let lastElm: HTMLElement | undefined = undefined; + for (const thing of state) { + if (typeof thing === "string") { + html.append(thing); + continue; + } + const {option, state} = thing; + const opt = option.toHTML(state, channel); + lastElm = opt; + html.append(opt); + } + if (lastElm) { + focusInput(lastElm); + } else { + const node = new Text(); + node.textContent = ""; + html.append(node); + focusElm(node, false); + } + } + stateChange(option: Option, channel: Channel, state: string) { + const states = this.state.get(channel); + if (!states) return; + const stateObj = states.find((_) => _ instanceof Object && _.option === option); + if (stateObj && stateObj instanceof Object) { + stateObj.state = state; + } + } + get info() { + return this.owner.info; + } + + get headers() { + return this.owner.headers; + } + + async submit(channel: Channel) { + const nonce = Math.floor(Math.random() * 10 ** 9) + ""; + const states = this.state.get(channel); + if (!states) { + return true; + } + const opts = states.filter((_) => typeof _ !== "string"); + + await fetch(this.info.api + "/interactions", { + method: "POST", + headers: this.headers, + body: JSON.stringify({ + type: 2, + nonce: nonce, + guild_id: channel.owner.id, + channel_id: channel.id, + application_id: this.applicationId, + session_id: this.localuser.session_id, + data: { + application_command: this.rawJson, + attachments: [], + id: this.id, + name: this.name, + options: opts.map(({option, state}) => { + return option.toJson(state); + }), + type: 1, + version: this.version, + }, + }), + }); + this.state.delete(channel); + return true; + } +} +abstract class Option { + type: number; + required: boolean; + private name: string; + private description: string; + private nameLocalizations: Record; + private descriptionLocalizations: Record; + constructor(optionjson: commandOptionJson) { + this.required = optionjson.required || false; + this.name = optionjson.name; + this.nameLocalizations = optionjson.name_localizations || {}; + this.description = optionjson.description; + this.descriptionLocalizations = optionjson.description_localizations || {}; + this.type = optionjson.type; + } + match(str: string) { + return str === this.name; + } + get localizedName() { + return this.nameLocalizations[I18n.lang] || this.name; + } + get localizedDescription() { + return this.descriptionLocalizations[I18n.lang] || this.description; + } + static toOption(optionjson: commandOptionJson, owner: Command): Option { + switch (optionjson.type) { + case 3: + return new StringOption(optionjson, owner); + default: + return new ErrorOption(optionjson); + } + } + abstract toHTML(state: string, channel: Channel): HTMLElement; + imprintName(html: HTMLElement) { + html.setAttribute("commandName", this.name); + } + similar(search: string) { + if (search.length === 0) { + return 0.1; + } + const similar = (str: string) => { + if (str.includes(search)) { + return search.length / str.length; + } else if (str.toLowerCase().includes(search.toLowerCase())) { + return str.length / str.length / 1.4; + } else { + return 0; + } + }; + return Math.max( + similar(this.name), + similar(this.description), + similar(this.localizedDescription), + similar(this.localizedName), + ); + } + toJson(state: string) { + return { + value: state, + type: this.type, + name: this.name, + }; + } +} +class ErrorOption extends Option { + constructor(optionjson: commandOptionJson) { + super(optionjson); + this.required = false; + } + toHTML(): HTMLElement { + const span = document.createElement("span"); + this.imprintName(span); + span.textContent = "Fermi doesn't impl this yet"; + return span; + } +} +class StringOption extends Option { + minLeng: number; + maxLeng: number; + choices: commandOptionJson["choices"]; + autocomplete: boolean; + owner: Command; + constructor(optionjson: commandOptionJson, owner: Command) { + super(optionjson); + this.owner = owner; + this.minLeng = optionjson.min_length || 0; + this.maxLeng = optionjson.min_length || 6000; + this.choices = optionjson.choices; + this.autocomplete = optionjson.autocomplete || false; + } + + toHTML(state: string, channel: Channel): HTMLElement { + const div = document.createElement("div"); + div.contentEditable = "false"; + div.classList.add("flexltr", "commandinput"); + this.imprintName(div); + + const label = document.createElement("span"); + label.textContent = this.localizedName + ":"; + + const input = document.createElement("input"); + input.type = "text"; + input.value = state; + input.onkeydown = (e) => { + if (input.selectionStart === 0 && e.key === "Backspace") { + const before = !!div.nextSibling; + const sib = div.nextSibling || div.previousSibling; + div.remove(); + focusElm(sib as HTMLElement, before); + e.preventDefault(); + e.stopImmediatePropagation(); + } + }; + input.onkeyup = (e) => { + if (input.selectionStart === input.value.length && e.key === "ArrowRight") { + focusElm(div, false); + } + this.owner.stateChange(this, channel, input.value); + }; + + div.append(label, input); + return div; + } +} diff --git a/src/webpage/compontents.ts b/src/webpage/interactions/compontents.ts similarity index 94% rename from src/webpage/compontents.ts rename to src/webpage/interactions/compontents.ts index ac317af..075b132 100644 --- a/src/webpage/compontents.ts +++ b/src/webpage/interactions/compontents.ts @@ -1,9 +1,9 @@ -import {Channel} from "./channel.js"; -import {I18n} from "./i18n.js"; -import {actionRow, button, component, select} from "./jsontypes.js"; -import {MarkDown} from "./markdown"; -import {Message} from "./message.js"; -import {FancySelect} from "./utils/fancySelect.js"; +import {Channel} from "../channel.js"; +import {I18n} from "../i18n.js"; +import {actionRow, button, component, select} from "../jsontypes.js"; +import {MarkDown} from "../markdown"; +import {Message} from "../message.js"; +import {FancySelect} from "../utils/fancySelect.js"; abstract class compObj { abstract owner: Components; abstract getHTML(): HTMLElement; diff --git a/src/webpage/jsontypes.ts b/src/webpage/jsontypes.ts index 946b6e1..ab182ba 100644 --- a/src/webpage/jsontypes.ts +++ b/src/webpage/jsontypes.ts @@ -206,6 +206,64 @@ export interface favandfreq { }; }; } +export interface applicationJson { + description: string; + flags: number; + icon: null | string; + id: string; + name: string; +} +export interface commandOptionJson { + type: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11; + name: string; + name_localized?: string; + name_localizations?: null | Record; + description: string; + description_localizations?: null | Record; + description_localized?: string; + choices?: + | { + name: string; + name_localizations?: null | Record; + name_localized?: string | null; + value: string | number; + }[] + | null; + options?: commandJson[]; + channel_types?: number[]; + min_value?: number; + max_value?: number; + min_length?: number; + max_length?: number; + autocomplete?: boolean; + required?: boolean; +} +export interface commandJson { + id: string; + type: 1 | 2 | 3 | 4; + application_id: string; + guild_id?: null | string; + name: string; + name_localized?: string; + name_localizations?: null | {[key: string]: string}; + description: string; + description_localizations?: null | {[key: string]: string}; + options?: commandOptionJson[]; + default_member_permissions?: null | string; + dm_permission: boolean; + permissions?: null | { + user?: boolean; + roles?: {[key: string]: boolean}; + channels?: {[key: string]: boolean}; + }; + nsfw: boolean; + integration_types?: null | number[]; + global_popularity_rank: number; + contexts?: null | number[]; + version: string; + handler: 0 | 1 | 2 | 3; //0 really shouldn't be here, but it's a bug and should be treated like 1. +} + interface readySuplemental { op: 0; t: "READY_SUPPLEMENTAL"; diff --git a/src/webpage/localuser.ts b/src/webpage/localuser.ts index 2c78a69..67297c3 100644 --- a/src/webpage/localuser.ts +++ b/src/webpage/localuser.ts @@ -3389,50 +3389,53 @@ class Localuser { MDSearchOptions( options: ( | [string, string, void | HTMLElement] - | [string, string, void | HTMLElement, () => void] + | [string, string, void | HTMLElement, () => void | boolean] )[], original: string, div: HTMLDivElement, - typebox: MarkDown, + typebox?: MarkDown, ) { if (!div) return; div.innerHTML = ""; let i = 0; const htmloptions: HTMLSpanElement[] = []; - for (const thing of options) { + for (const [name, replace, elm, func] of options) { if (i == 8) { break; } i++; const span = document.createElement("span"); htmloptions.push(span); - if (thing[2]) { - span.append(thing[2]); + if (elm) { + span.append(elm); } - span.append(thing[0]); + span.append(name); span.onclick = (e) => { if (e) { - const selection = window.getSelection() as Selection; - const box = typebox.box.deref(); - if (!box) return; - if (selection) { - const pos = getTextNodeAtPosition( - box, - original.length - - (original.match(this.autofillregex) as RegExpMatchArray)[0].length + - thing[1].length, - ); - selection.removeAllRanges(); - const range = new Range(); - range.setStart(pos.node, pos.position); - selection.addRange(range); + if (replace) { + const selection = window.getSelection() as Selection; + const box = typebox?.box.deref(); + if (!box) return; + if (selection) { + const pos = getTextNodeAtPosition( + box, + original.length - + (original.match(this.autofillregex) as RegExpMatchArray)[0].length + + replace.length, + ); + selection.removeAllRanges(); + const range = new Range(); + range.setStart(pos.node, pos.position); + selection.addRange(range); + } + box.focus(); } e.preventDefault(); - box.focus(); } - this.MDReplace(thing[1], original, typebox); - thing[3]?.(); + if (!func?.() && typebox) { + this.MDReplace(replace, original, typebox); + } div.innerHTML = ""; remove(); }; @@ -3508,15 +3511,6 @@ class Localuser { typebox, ); } - async getUser(id: string) { - if (this.userMap.has(id)) { - return this.userMap.get(id) as User; - } - return new User( - await (await fetch(this.info.api + "/users/" + id, {headers: this.headers})).json(), - this, - ); - } MDFineMentionGen(name: string, original: string, box: HTMLDivElement, typebox: MarkDown) { let members: [Member | Role | User | "@everyone", number][] = []; if (this.lookingguild && name !== "everyone") { @@ -3634,6 +3628,34 @@ class Localuser { }); this.MDSearchOptions(map, orginal, box, typebox); } + async findCommands(search: string, box: HTMLDivElement, md: MarkDown) { + const guild = this.lookingguild; + if (!guild) return; + const commands = await guild.getCommands(); + const sorted = commands + .map((_) => [_, _.similar(search)] as const) + .filter((_) => _[1] !== 0) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + + this.MDSearchOptions( + sorted.map(([elm]) => { + return [ + `/${elm.localizedName}`, + "", + undefined, + () => { + this.channelfocus?.startCommand(elm); + return true; + }, + ] as const; + }), + "", + box, + md, + ); + console.log(sorted, search); + } search(box: HTMLDivElement, md: MarkDown, str: string, pre: boolean) { if (!pre) { const match = str.match(this.autofillregex); @@ -3660,6 +3682,11 @@ class Localuser { } return; } + const command = str.match(/^\/((\s*[\w\d]+)*)$/); + if (command) { + const search = command[1]; + this.findCommands(search, box, md); + } } box.innerHTML = ""; } diff --git a/src/webpage/markdown.ts b/src/webpage/markdown.ts index c2b4ca9..dadb0f4 100644 --- a/src/webpage/markdown.ts +++ b/src/webpage/markdown.ts @@ -973,7 +973,9 @@ class MarkDown { ) { this.customBox = [stringToHTML, HTMLToString]; } + boxEnabled = true; boxupdate(offset = 0, allowLazy = true, computedLength: void | number = undefined) { + if (!this.boxEnabled) return; const box = this.box.deref(); if (!box) return; let restore: undefined | (() => void); diff --git a/src/webpage/message.ts b/src/webpage/message.ts index b7bdbe2..4a7c71f 100644 --- a/src/webpage/message.ts +++ b/src/webpage/message.ts @@ -15,8 +15,7 @@ import {I18n} from "./i18n.js"; import {Hover} from "./hover.js"; import {Dialog} from "./settings.js"; import {Sticker} from "./sticker.js"; -import {Components} from "./compontents.js"; - +import {Components} from "./interactions/compontents.js"; class Message extends SnowFlake { static contextmenu = new Contextmenu("message menu"); stickers!: Sticker[]; diff --git a/src/webpage/style.css b/src/webpage/style.css index ee690c4..0d6f244 100644 --- a/src/webpage/style.css +++ b/src/webpage/style.css @@ -163,6 +163,22 @@ body { justify-content: space-between; padding: 4px 14px; } +.commandinput { + display: inline-flex !important; + width: fit-content; + margin-left: 6px; + align-items: center; + background: var(--primary-bg); + padding-left: 4px; + border-radius: 4px; + input { + padding: 5px !important; + margin: 0px !important; + margin-left: 5px !important; + height: 100%; + field-sizing: content; + } +} .flexltr { min-height: 0; display: flex; @@ -1955,6 +1971,12 @@ span.instanceStatus { #realbox { padding: 0 16px 28px 16px; } +.commandFront { + user-select: none !important; + padding: 4px; + background: var(--secondary-bg); + border-radius: 4px; +} #typebox { margin: -10px 0px; flex-grow: 1;