Merge branch 'dev'
This commit is contained in:
commit
6bce0f152d
|
|
@ -1682,9 +1682,9 @@ const server = Bun.serve<WSData>({
|
|||
|
||||
if (spaceConfig) {
|
||||
const vis = spaceConfig.visibility;
|
||||
if (vis === "authenticated" || vis === "members_only") {
|
||||
if (vis === "permissioned" || vis === "private") {
|
||||
if (!claims) return new Response("Authentication required", { status: 401 });
|
||||
} else if (vis === "public_read") {
|
||||
} else if (vis === "public") {
|
||||
readOnly = !claims;
|
||||
}
|
||||
}
|
||||
|
|
@ -1701,8 +1701,7 @@ const server = Bun.serve<WSData>({
|
|||
// Non-member defaults by visibility
|
||||
const vis = spaceConfig?.visibility;
|
||||
if (vis === 'public' && claims) spaceRole = 'member';
|
||||
else if (vis === 'public_read' && claims) spaceRole = 'member';
|
||||
else if (vis === 'authenticated' && claims) spaceRole = 'viewer';
|
||||
else if (vis === 'permissioned' && claims) spaceRole = 'viewer';
|
||||
else if (claims) spaceRole = 'viewer';
|
||||
else spaceRole = 'viewer'; // anonymous
|
||||
}
|
||||
|
|
@ -1781,7 +1780,7 @@ const server = Bun.serve<WSData>({
|
|||
name: `${claims.username}'s Space`,
|
||||
slug: subdomain,
|
||||
ownerDID: claims.sub,
|
||||
visibility: "members_only",
|
||||
visibility: "private",
|
||||
source: 'subdomain',
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -623,7 +623,6 @@ spaces.patch("/:slug", async (c) => {
|
|||
|
||||
if (body.visibility) {
|
||||
const valid: SpaceVisibility[] = ["public", "permissioned", "private"];
|
||||
// Note: the create endpoint (line ~276) already validates correctly
|
||||
if (!valid.includes(body.visibility)) {
|
||||
return c.json({ error: `Invalid visibility. Must be one of: ${valid.join(", ")}` }, 400);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,20 +115,20 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
}
|
||||
|
||||
#visibilityInfo(s: SpaceInfo): { cls: string; label: string } {
|
||||
const v = s.visibility || "public_read";
|
||||
if (v === "members_only") return { cls: "vis-private", label: "🔒" };
|
||||
if (v === "authenticated") return { cls: "vis-permissioned", label: "🔑" };
|
||||
const v = s.visibility || "public";
|
||||
if (v === "private") return { cls: "vis-private", label: "🔒" };
|
||||
if (v === "permissioned") return { cls: "vis-permissioned", label: "🔑" };
|
||||
return { cls: "vis-public", label: "👁" };
|
||||
}
|
||||
|
||||
/** Format display name based on visibility type */
|
||||
#displayName(s: SpaceInfo): string {
|
||||
const v = s.visibility || "public_read";
|
||||
if (v === "members_only") {
|
||||
const v = s.visibility || "public";
|
||||
if (v === "private") {
|
||||
const username = getUsername();
|
||||
return username ? `${username}'s (you)rSpace` : "(you)rSpace";
|
||||
return username ? `${username}'s Space` : "My Space";
|
||||
}
|
||||
return `${s.name} rSpace`;
|
||||
return s.name;
|
||||
}
|
||||
|
||||
#renderMenu(menu: HTMLElement, current: string) {
|
||||
|
|
@ -140,7 +140,7 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
cta = this.#yourSpaceCTAhtml("Sign in to create →");
|
||||
} else {
|
||||
const username = getUsername();
|
||||
const label = username ? `Create ${username}'s Space →` : "Create (you)rSpace →";
|
||||
const label = username ? `Create ${username}'s Space →` : "Create My Space →";
|
||||
cta = this.#yourSpaceCTAhtml(label);
|
||||
}
|
||||
menu.innerHTML = `
|
||||
|
|
@ -149,9 +149,11 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
<div class="menu-empty">
|
||||
${auth ? "No spaces yet" : "Sign in to see your spaces"}
|
||||
</div>
|
||||
<a class="item item--create" href="/new">+ Create new space</a>
|
||||
<button class="item item--create" id="create-toggle">+ Create new space</button>
|
||||
${this.#createFormHTML()}
|
||||
`;
|
||||
this.#attachYourSpaceCTA(menu);
|
||||
this.#attachCreatePopout(menu);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -166,13 +168,13 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
|
||||
let html = "";
|
||||
|
||||
// ── Create (you)rSpace CTA — only if user has no owned spaces ──
|
||||
// ── Create personal space CTA — only if user has no owned spaces ──
|
||||
if (!auth) {
|
||||
html += this.#yourSpaceCTAhtml("Sign in to create →");
|
||||
html += `<div class="divider"></div>`;
|
||||
} else if (!hasOwnedSpace) {
|
||||
const username = getUsername();
|
||||
const label = username ? `Create ${username}'s Space →` : "Create (you)rSpace →";
|
||||
const label = username ? `Create ${username}'s Space →` : "Create My Space →";
|
||||
html += this.#yourSpaceCTAhtml(label);
|
||||
html += `<div class="divider"></div>`;
|
||||
}
|
||||
|
|
@ -237,7 +239,8 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
}
|
||||
|
||||
html += `<div class="divider"></div>`;
|
||||
html += `<a class="item item--create" href="/new">+ Create new space</a>`;
|
||||
html += `<button class="item item--create" id="create-toggle">+ Create new space</button>`;
|
||||
html += this.#createFormHTML();
|
||||
|
||||
menu.innerHTML = html;
|
||||
|
||||
|
|
@ -260,19 +263,41 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
});
|
||||
});
|
||||
|
||||
// Attach "(you)rSpace" CTA listener
|
||||
// Attach personal space CTA listener
|
||||
this.#attachYourSpaceCTA(menu);
|
||||
|
||||
// Attach create-space popout
|
||||
this.#attachCreatePopout(menu);
|
||||
}
|
||||
|
||||
#yourSpaceCTAhtml(buttonLabel: string): string {
|
||||
const username = getUsername();
|
||||
const title = username ? `${username}'s Space` : "My Space";
|
||||
return `
|
||||
<div class="item item--yourspace vis-private">
|
||||
<span class="item-icon">🔒</span>
|
||||
<span class="item-name">(you)rSpace</span>
|
||||
<span class="item-name">${title}</span>
|
||||
<button class="yourspace-btn" id="yourspace-cta">${buttonLabel}</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
#createFormHTML(): string {
|
||||
return `<div class="create-popout hidden" id="create-popout">
|
||||
<input class="create-input" id="create-name" placeholder="Space name" maxlength="60" />
|
||||
<div class="create-slug" id="create-slug"></div>
|
||||
<div class="create-vis-row">
|
||||
<label class="create-vis-opt"><input type="radio" name="create-vis" value="public" checked /><span class="vis-dot vis-dot--public"></span> Public</label>
|
||||
<label class="create-vis-opt"><input type="radio" name="create-vis" value="permissioned" /><span class="vis-dot vis-dot--permissioned"></span> Permissioned</label>
|
||||
<label class="create-vis-opt"><input type="radio" name="create-vis" value="private" /><span class="vis-dot vis-dot--private"></span> Private</label>
|
||||
</div>
|
||||
<div class="create-actions">
|
||||
<button class="create-btn" id="create-submit">Create</button>
|
||||
<button class="create-cancel" id="create-cancel">Cancel</button>
|
||||
</div>
|
||||
<div class="create-status" id="create-status"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
#attachYourSpaceCTA(menu: HTMLElement) {
|
||||
const btn = menu.querySelector("#yourspace-cta");
|
||||
if (!btn) return;
|
||||
|
|
@ -294,6 +319,79 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
});
|
||||
}
|
||||
|
||||
#attachCreatePopout(menu: HTMLElement) {
|
||||
const toggle = menu.querySelector("#create-toggle") as HTMLElement;
|
||||
const popout = menu.querySelector("#create-popout") as HTMLElement;
|
||||
const nameInput = menu.querySelector("#create-name") as HTMLInputElement;
|
||||
const slugPreview = menu.querySelector("#create-slug") as HTMLElement;
|
||||
const submitBtn = menu.querySelector("#create-submit") as HTMLButtonElement;
|
||||
const cancelBtn = menu.querySelector("#create-cancel") as HTMLElement;
|
||||
const status = menu.querySelector("#create-status") as HTMLElement;
|
||||
if (!toggle || !popout) return;
|
||||
|
||||
const toSlug = (name: string) =>
|
||||
name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
|
||||
|
||||
toggle.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const showing = popout.classList.toggle("hidden");
|
||||
if (!showing) nameInput.focus();
|
||||
});
|
||||
|
||||
nameInput.addEventListener("input", () => {
|
||||
const slug = toSlug(nameInput.value);
|
||||
slugPreview.textContent = slug ? `slug: ${slug}` : "";
|
||||
});
|
||||
|
||||
nameInput.addEventListener("click", (e) => e.stopPropagation());
|
||||
|
||||
cancelBtn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
popout.classList.add("hidden");
|
||||
nameInput.value = "";
|
||||
slugPreview.textContent = "";
|
||||
status.textContent = "";
|
||||
});
|
||||
|
||||
submitBtn.addEventListener("click", async (e) => {
|
||||
e.stopPropagation();
|
||||
const name = nameInput.value.trim();
|
||||
if (!name) { status.textContent = "Name required"; return; }
|
||||
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
const identity = document.querySelector("rstack-identity") as any;
|
||||
if (identity?.showAuthModal) identity.showAuthModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const slug = toSlug(name);
|
||||
const vis = (menu.querySelector('input[name="create-vis"]:checked') as HTMLInputElement)?.value || "public";
|
||||
|
||||
submitBtn.disabled = true;
|
||||
status.textContent = "Creating...";
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/spaces", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, slug, visibility: vis }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok && data.slug) {
|
||||
window.location.href = rspaceNavUrl(data.slug, "canvas");
|
||||
} else {
|
||||
status.textContent = data.error || "Failed to create space";
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
} catch {
|
||||
status.textContent = "Network error";
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async #autoProvision() {
|
||||
const token = getAccessToken();
|
||||
if (!token) return;
|
||||
|
|
@ -407,10 +505,9 @@ export class RStackSpaceSwitcher extends HTMLElement {
|
|||
<input class="input" id="es-name" value="${spaceName.replace(/"/g, """)}" />
|
||||
<label class="field-label">Visibility</label>
|
||||
<select class="input" id="es-visibility">
|
||||
<option value="public">Public (read + write)</option>
|
||||
<option value="public_read">Public (read only)</option>
|
||||
<option value="authenticated">Authenticated users</option>
|
||||
<option value="members_only">Members only</option>
|
||||
<option value="public">👁 Public — anyone can read, sign in to write</option>
|
||||
<option value="permissioned">🔑 Permissioned — sign in to read & write</option>
|
||||
<option value="private">🔒 Private — invite-only</option>
|
||||
</select>
|
||||
<label class="field-label">Description</label>
|
||||
<textarea class="input" id="es-description" rows="3" placeholder="Optional description..." style="resize:vertical"></textarea>
|
||||
|
|
@ -910,10 +1007,58 @@ const STYLES = `
|
|||
.item--create {
|
||||
font-size: 0.85rem; font-weight: 600; color: #06b6d4 !important;
|
||||
border-left-color: transparent !important;
|
||||
background: none; border: none; width: 100%; text-align: left; cursor: pointer;
|
||||
font-family: inherit;
|
||||
}
|
||||
.item--create:hover { background: rgba(6,182,212,0.08) !important; }
|
||||
|
||||
/* (you)rSpace CTA */
|
||||
/* Create-space popout */
|
||||
.create-popout { padding: 8px 12px; }
|
||||
.create-popout.hidden { display: none; }
|
||||
.create-input {
|
||||
width: 100%; padding: 8px 10px; border-radius: 6px;
|
||||
border: 1px solid var(--rs-border); background: var(--rs-bg-surface-sunken);
|
||||
color: var(--rs-text-primary); font-size: 0.85rem; outline: none;
|
||||
box-sizing: border-box; font-family: inherit;
|
||||
}
|
||||
.create-input:focus { border-color: #06b6d4; }
|
||||
.create-slug {
|
||||
font-size: 0.7rem; color: var(--rs-text-muted); padding: 2px 2px 6px;
|
||||
min-height: 1em;
|
||||
}
|
||||
.create-vis-row {
|
||||
display: flex; gap: 10px; margin-bottom: 8px; flex-wrap: wrap;
|
||||
}
|
||||
.create-vis-opt {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 0.78rem; color: var(--rs-text-secondary); cursor: pointer;
|
||||
}
|
||||
.create-vis-opt input { margin: 0; }
|
||||
.vis-dot {
|
||||
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
|
||||
}
|
||||
.vis-dot--public { background: #34d399; }
|
||||
.vis-dot--permissioned { background: #fbbf24; }
|
||||
.vis-dot--private { background: #f87171; }
|
||||
.create-actions { display: flex; gap: 8px; }
|
||||
.create-btn {
|
||||
padding: 6px 16px; border-radius: 6px; border: none;
|
||||
font-size: 0.8rem; font-weight: 600; cursor: pointer;
|
||||
background: linear-gradient(135deg, #06b6d4, #7c3aed); color: white;
|
||||
}
|
||||
.create-btn:hover { opacity: 0.85; }
|
||||
.create-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.create-cancel {
|
||||
padding: 6px 12px; border-radius: 6px; border: none;
|
||||
font-size: 0.8rem; cursor: pointer;
|
||||
background: var(--rs-btn-secondary-bg); color: var(--rs-text-secondary);
|
||||
}
|
||||
.create-cancel:hover { background: var(--rs-bg-hover); }
|
||||
.create-status {
|
||||
font-size: 0.75rem; color: #f87171; margin-top: 4px; min-height: 1em;
|
||||
}
|
||||
|
||||
/* Personal space CTA */
|
||||
.item--yourspace {
|
||||
border-left-color: #f87171; padding: 12px 14px;
|
||||
background: var(--rs-bg-hover);
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ export class RStackTabBar extends HTMLElement {
|
|||
#wiringSourceFeedId = "";
|
||||
#wiringSourceKind: FlowKind | null = null;
|
||||
#escHandler: ((e: KeyboardEvent) => void) | null = null;
|
||||
// Recent apps: moduleId → last-used timestamp
|
||||
#recentApps: Map<string, number> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
|
@ -141,6 +143,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.#loadRecentApps();
|
||||
this.#render();
|
||||
}
|
||||
|
||||
|
|
@ -152,6 +155,12 @@ export class RStackTabBar extends HTMLElement {
|
|||
/** Set the layer list (call from outside) */
|
||||
setLayers(layers: Layer[]) {
|
||||
this.#layers = [...layers].sort((a, b) => a.order - b.order);
|
||||
// Seed recent apps from existing layers
|
||||
for (const l of layers) {
|
||||
if (!this.#recentApps.has(l.moduleId)) {
|
||||
this.#recentApps.set(l.moduleId, l.createdAt || 0);
|
||||
}
|
||||
}
|
||||
this.#render();
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +169,36 @@ export class RStackTabBar extends HTMLElement {
|
|||
this.#modules = modules;
|
||||
}
|
||||
|
||||
// ── Recent apps persistence ──
|
||||
|
||||
#recentAppsKey(): string {
|
||||
return `rspace_recent_apps_${this.space || "default"}`;
|
||||
}
|
||||
|
||||
#loadRecentApps() {
|
||||
try {
|
||||
const raw = localStorage.getItem(this.#recentAppsKey());
|
||||
if (raw) {
|
||||
const obj = JSON.parse(raw) as Record<string, number>;
|
||||
this.#recentApps = new Map(Object.entries(obj));
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
#saveRecentApps() {
|
||||
try {
|
||||
const obj: Record<string, number> = {};
|
||||
for (const [k, v] of this.#recentApps) obj[k] = v;
|
||||
localStorage.setItem(this.#recentAppsKey(), JSON.stringify(obj));
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
/** Record a module as recently used (call from outside or internally) */
|
||||
trackRecent(moduleId: string) {
|
||||
this.#recentApps.set(moduleId, Date.now());
|
||||
this.#saveRecentApps();
|
||||
}
|
||||
|
||||
/** Set the inter-layer flows (for stack view) */
|
||||
setFlows(flows: LayerFlow[]) {
|
||||
this.#flows = flows;
|
||||
|
|
@ -302,7 +341,23 @@ export class RStackTabBar extends HTMLElement {
|
|||
return `<div class="add-menu" id="add-menu"><div class="add-menu-empty">No rApps available</div></div>`;
|
||||
}
|
||||
|
||||
// Group by category
|
||||
let html = "";
|
||||
|
||||
// ── Recent apps section (top of menu) ──
|
||||
if (this.#recentApps.size > 0) {
|
||||
const recentEntries = [...this.#recentApps.entries()]
|
||||
.sort((a, b) => b[1] - a[1]) // newest first
|
||||
.slice(0, 6); // show up to 6 recent
|
||||
const recentModules = recentEntries
|
||||
.map(([id]) => allModules.find(m => m.id === id))
|
||||
.filter((m): m is typeof allModules[0] => !!m);
|
||||
if (recentModules.length > 0) {
|
||||
html += `<div class="add-menu-category">Recent</div>`;
|
||||
html += recentModules.map(m => this.#renderAddMenuItem(m, existingModuleIds.has(m.id))).join("");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Group by category ──
|
||||
const groups = new Map<string, typeof allModules>();
|
||||
const uncategorized: typeof allModules = [];
|
||||
for (const m of allModules) {
|
||||
|
|
@ -315,7 +370,6 @@ export class RStackTabBar extends HTMLElement {
|
|||
}
|
||||
}
|
||||
|
||||
let html = "";
|
||||
for (const cat of CATEGORY_ORDER) {
|
||||
const items = groups.get(cat);
|
||||
if (!items || items.length === 0) continue;
|
||||
|
|
@ -741,6 +795,7 @@ export class RStackTabBar extends HTMLElement {
|
|||
const layerId = tab.dataset.layerId!;
|
||||
const moduleId = tab.dataset.moduleId!;
|
||||
this.active = layerId;
|
||||
this.trackRecent(moduleId);
|
||||
this.dispatchEvent(new CustomEvent("layer-switch", {
|
||||
detail: { layerId, moduleId },
|
||||
bubbles: true,
|
||||
|
|
@ -794,21 +849,26 @@ export class RStackTabBar extends HTMLElement {
|
|||
});
|
||||
});
|
||||
|
||||
// Add button
|
||||
// Add button — click + touch support
|
||||
const addBtn = this.#shadow.getElementById("add-btn");
|
||||
addBtn?.addEventListener("click", (e) => {
|
||||
const toggleAddMenu = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.#addMenuOpen = !this.#addMenuOpen;
|
||||
this.#render();
|
||||
});
|
||||
};
|
||||
addBtn?.addEventListener("click", toggleAddMenu);
|
||||
addBtn?.addEventListener("touchend", toggleAddMenu);
|
||||
|
||||
// Add menu items
|
||||
// Add menu items — click + touch support
|
||||
this.#shadow.querySelectorAll<HTMLElement>(".add-menu-item").forEach(item => {
|
||||
item.addEventListener("click", (e) => {
|
||||
const handleSelect = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const moduleId = item.dataset.addModule!;
|
||||
const isOpen = item.dataset.moduleOpen === "true";
|
||||
this.#addMenuOpen = false;
|
||||
this.trackRecent(moduleId);
|
||||
|
||||
if (isOpen) {
|
||||
// Surface existing tab instead of adding a duplicate
|
||||
|
|
@ -826,17 +886,23 @@ export class RStackTabBar extends HTMLElement {
|
|||
bubbles: true,
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
item.addEventListener("click", handleSelect);
|
||||
item.addEventListener("touchend", handleSelect);
|
||||
});
|
||||
|
||||
// Close add menu on outside click
|
||||
// Close add menu on outside click/touch
|
||||
if (this.#addMenuOpen) {
|
||||
const handler = () => {
|
||||
this.#addMenuOpen = false;
|
||||
this.#render();
|
||||
document.removeEventListener("click", handler);
|
||||
document.removeEventListener("touchend", handler);
|
||||
};
|
||||
setTimeout(() => document.addEventListener("click", handler), 0);
|
||||
setTimeout(() => {
|
||||
document.addEventListener("click", handler);
|
||||
document.addEventListener("touchend", handler);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// View toggle
|
||||
|
|
@ -1114,9 +1180,9 @@ const STYLES = `
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.tabs-scroll {
|
||||
|
|
@ -1126,6 +1192,7 @@ const STYLES = `
|
|||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
scrollbar-width: none;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -1229,19 +1296,25 @@ const STYLES = `
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
border: 1px dashed rgba(148,163,184,0.3);
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.tab-add:hover {
|
||||
.tab-add:hover, .tab-add:active {
|
||||
border-color: #22d3ee;
|
||||
color: #22d3ee;
|
||||
background: rgba(34,211,238,0.08);
|
||||
|
|
@ -1287,7 +1360,8 @@ const STYLES = `
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px 10px;
|
||||
padding: 8px 10px;
|
||||
min-height: 40px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
|
|
@ -1296,8 +1370,10 @@ const STYLES = `
|
|||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background 0.12s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
.add-menu-item:hover { background: var(--rs-bg-hover); }
|
||||
.add-menu-item:hover, .add-menu-item:active { background: var(--rs-bg-hover); }
|
||||
|
||||
.add-menu-badge {
|
||||
display: inline-flex;
|
||||
|
|
|
|||
Loading…
Reference in New Issue