Fix skills.json determinism (CI blocker) and upgrade the playground
The check-generated CI step was failing with "web/skills.json is stale" because build-skills.mjs stamped a wall-clock generatedAt into the file, so every rebuild differed and git diff --exit-code never matched. - web/build-skills.mjs: drop the unused generatedAt timestamp -> deterministic output the CI staleness check can verify. Also tags each skill with its tier. - skill-tiers.json: single machine-readable source for tier membership (Production-Ready / Experimental); TIERS.md points to it. Playground upgrades (hosted on GitHub Pages): - Tier filter (Production-Ready / Stable / Experimental) + per-tile tier badges. - "Use this skill in another tool" panel: copy the instructions formatted for ChatGPT, Gemini, or raw — mirrors the generated exports/ files. - web/README documents the new options and the deterministic build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016JWn5jRD5tcEFKrubjQ6Px
This commit is contained in:
@@ -33,6 +33,15 @@ each new wave of skills bumps the **major** version, extensions and fixes bump
|
|||||||
strongest work.
|
strongest work.
|
||||||
- **Cross-tool compatibility** — README now documents which platforms the skills work
|
- **Cross-tool compatibility** — README now documents which platforms the skills work
|
||||||
on (Claude Code natively; the SKILL.md bodies port to other agents and chat LLMs).
|
on (Claude Code natively; the SKILL.md bodies port to other agents and chat LLMs).
|
||||||
|
- **Skill Playground upgrades** — the hosted web app gains a **tier filter** and per-tile
|
||||||
|
tier badges, plus a *"Use this skill in another tool"* panel that copies the
|
||||||
|
instructions formatted for ChatGPT, Gemini, or raw. Tier data comes from a single
|
||||||
|
machine-readable source, `skill-tiers.json`.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`web/skills.json` is now deterministic.** Removed the wall-clock `generatedAt` field
|
||||||
|
(it was unused by the UI and made every rebuild differ), so the new `check-generated`
|
||||||
|
CI step can reliably verify the index is in sync with the source skills.
|
||||||
- **Related Projects** — README section linking to other community Claude Skills
|
- **Related Projects** — README section linking to other community Claude Skills
|
||||||
libraries and the `awesome-claude-skills` / `awesome-claude-code` lists.
|
libraries and the `awesome-claude-skills` / `awesome-claude-code` lists.
|
||||||
|
|
||||||
|
|||||||
@@ -81,3 +81,7 @@ list in the [README](README.md#️-all-167-skills).
|
|||||||
once they have a stable output format and real-world use — see
|
once they have a stable output format and real-world use — see
|
||||||
[SKILL-AUTHORING-STANDARD.md](SKILL-AUTHORING-STANDARD.md#7-tiering). Think a skill is
|
[SKILL-AUTHORING-STANDARD.md](SKILL-AUTHORING-STANDARD.md#7-tiering). Think a skill is
|
||||||
mis-tiered? [Open an issue](../../issues).*
|
mis-tiered? [Open an issue](../../issues).*
|
||||||
|
|
||||||
|
> **For tooling:** the machine-readable tier membership lives in
|
||||||
|
> [`skill-tiers.json`](skill-tiers.json) (the Skill Playground reads it to badge and
|
||||||
|
> filter skills). Keep this page and that file in sync when re-tiering.
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Machine-readable source for skill tiers. Keep in sync with TIERS.md. Any skill not listed here is 'stable'. Consumed by web/build-skills.mjs to tag skills.json.",
|
||||||
|
"productionReady": [
|
||||||
|
"prd-template", "meeting-notes", "stakeholder-update", "user-research-synthesis", "competitive-analysis",
|
||||||
|
"rice-prioritisation", "feature-prioritisation", "okr-builder", "roadmap-narrative", "rice-impact-matrix",
|
||||||
|
"sprint-planning", "sprint-brief", "user-story-writer", "retro-analysis", "ab-test-planner", "product-launch-checklist", "technical-spec-template",
|
||||||
|
"customer-journey-map", "assumption-mapper", "user-interview-synthesis", "discovery-interview-guide", "job-story-mapper",
|
||||||
|
"data-analysis-standard", "retention-analysis", "cohort-analysis", "metrics-framework", "product-health-analysis",
|
||||||
|
"cs-health-scorecard", "churn-analysis", "qbr-deck", "renewal-playbook", "customer-success-plan", "cs-escalation-brief",
|
||||||
|
"code-review-checklist", "incident-postmortem", "architecture-decision-record", "api-docs-writer", "runbook-writer", "changelog-generator", "pr-description-writer", "technical-debt-register",
|
||||||
|
"go-to-market", "competitor-teardown", "product-positioning-doc",
|
||||||
|
"executive-summary", "press-release"
|
||||||
|
],
|
||||||
|
"experimental": [
|
||||||
|
"instagram-post-downloader", "substack-notes-scraper", "thumbnail-creator", "notebooklm-connector",
|
||||||
|
"email-triage", "morning-intelligence", "last-30-days-research", "competitor-signal-tracker",
|
||||||
|
"multi-source-signal-synthesiser"
|
||||||
|
]
|
||||||
|
}
|
||||||
+12
-1
@@ -5,6 +5,15 @@ Pick a skill → it becomes a form → fill it in → Claude executes the skill'
|
|||||||
and streams the result. Your key is stored only in your browser (`localStorage`) and sent
|
and streams the result. Your key is stored only in your browser (`localStorage`) and sent
|
||||||
directly to `api.anthropic.com`. Nothing touches a server we own.
|
directly to `api.anthropic.com`. Nothing touches a server we own.
|
||||||
|
|
||||||
|
## What you can do
|
||||||
|
|
||||||
|
- **Search and filter** the full library by keyword, **bundle**, and **maturity tier**
|
||||||
|
(🟢 Production-Ready · 🔵 Stable · 🟡 Experimental) — every tile shows its tier.
|
||||||
|
- **Run a skill** against the Claude API and stream the output (copy or download as `.md`).
|
||||||
|
- **Use it in another tool** — each skill has a *"Use this skill in another tool"* panel
|
||||||
|
that copies the instructions formatted for **ChatGPT**, **Gemini**, or as raw text, so
|
||||||
|
you can paste it into any assistant. (Same output as the generated `exports/` files.)
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -20,7 +29,9 @@ Paste a key from [console.anthropic.com](https://console.anthropic.com/settings/
|
|||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
- `build-skills.mjs` scans `../skills/*/SKILL.md`, parses the frontmatter and the
|
- `build-skills.mjs` scans `../skills/*/SKILL.md`, parses the frontmatter and the
|
||||||
**Required Inputs** section, and writes `skills.json` (the UI's data source).
|
**Required Inputs** section, tags each skill with its tier (from `../skill-tiers.json`),
|
||||||
|
and writes a **deterministic** `skills.json` (the UI's data source — no timestamp, so CI
|
||||||
|
can verify it stays in sync).
|
||||||
- `app.js` sends the skill's instruction body as the `system` prompt and the filled-in
|
- `app.js` sends the skill's instruction body as the `system` prompt and the filled-in
|
||||||
fields as the user message, using the Anthropic Messages API with
|
fields as the user message, using the Anthropic Messages API with
|
||||||
`anthropic-dangerous-direct-browser-access: true` for direct browser calls.
|
`anthropic-dangerous-direct-browser-access: true` for direct browser calls.
|
||||||
|
|||||||
+49
-1
@@ -9,6 +9,12 @@ let SKILLS = [];
|
|||||||
let current = null;
|
let current = null;
|
||||||
let controller = null;
|
let controller = null;
|
||||||
|
|
||||||
|
const TIER_META = {
|
||||||
|
production: { label: 'Production-Ready', cls: 'tier-production', dot: '🟢' },
|
||||||
|
stable: { label: 'Stable', cls: 'tier-stable', dot: '🔵' },
|
||||||
|
experimental: { label: 'Experimental', cls: 'tier-experimental', dot: '🟡' },
|
||||||
|
};
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -28,12 +34,18 @@ async function init() {
|
|||||||
|
|
||||||
el('search').addEventListener('input', renderGallery);
|
el('search').addEventListener('input', renderGallery);
|
||||||
el('pluginFilter').addEventListener('change', renderGallery);
|
el('pluginFilter').addEventListener('change', renderGallery);
|
||||||
|
el('tierFilter').addEventListener('change', renderGallery);
|
||||||
el('backBtn').addEventListener('click', showGallery);
|
el('backBtn').addEventListener('click', showGallery);
|
||||||
el('runBtn').addEventListener('click', run);
|
el('runBtn').addEventListener('click', run);
|
||||||
el('stopBtn').addEventListener('click', () => controller && controller.abort());
|
el('stopBtn').addEventListener('click', () => controller && controller.abort());
|
||||||
el('copyBtn').addEventListener('click', () => navigator.clipboard.writeText(el('output').dataset.raw || ''));
|
el('copyBtn').addEventListener('click', () => navigator.clipboard.writeText(el('output').dataset.raw || ''));
|
||||||
el('downloadBtn').addEventListener('click', downloadOutput);
|
el('downloadBtn').addEventListener('click', downloadOutput);
|
||||||
|
|
||||||
|
// Copy the skill's instructions formatted for another assistant.
|
||||||
|
el('copyChatgpt').addEventListener('click', () => copyPrompt('chatgpt'));
|
||||||
|
el('copyGemini').addEventListener('click', () => copyPrompt('gemini'));
|
||||||
|
el('copyClaude').addEventListener('click', () => copyPrompt('claude'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('skills.json');
|
const res = await fetch('skills.json');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -58,11 +70,13 @@ async function init() {
|
|||||||
function renderGallery() {
|
function renderGallery() {
|
||||||
const q = el('search').value.toLowerCase().trim();
|
const q = el('search').value.toLowerCase().trim();
|
||||||
const bundle = el('pluginFilter').value;
|
const bundle = el('pluginFilter').value;
|
||||||
|
const tier = el('tierFilter').value;
|
||||||
const gallery = el('gallery');
|
const gallery = el('gallery');
|
||||||
gallery.innerHTML = '';
|
gallery.innerHTML = '';
|
||||||
|
|
||||||
const matches = SKILLS.filter((s) => {
|
const matches = SKILLS.filter((s) => {
|
||||||
if (bundle && s.plugin !== bundle) return false;
|
if (bundle && s.plugin !== bundle) return false;
|
||||||
|
if (tier && (s.tier || 'stable') !== tier) return false;
|
||||||
if (!q) return true;
|
if (!q) return true;
|
||||||
return (s.title + ' ' + s.description + ' ' + s.name).toLowerCase().includes(q);
|
return (s.title + ' ' + s.description + ' ' + s.name).toLowerCase().includes(q);
|
||||||
});
|
});
|
||||||
@@ -76,11 +90,16 @@ function renderGallery() {
|
|||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
const frag = document.createDocumentFragment();
|
||||||
for (const s of matches) {
|
for (const s of matches) {
|
||||||
|
const meta = TIER_META[s.tier] || TIER_META.stable;
|
||||||
const card = document.createElement('button');
|
const card = document.createElement('button');
|
||||||
card.className = 'skill-card';
|
card.className = 'skill-card';
|
||||||
card.innerHTML =
|
card.innerHTML =
|
||||||
`<span class="card-bundle"></span><h3 class="card-title"></h3><p class="card-summary"></p>`;
|
`<div class="card-tags"><span class="card-bundle"></span><span class="card-tier"></span></div>` +
|
||||||
|
`<h3 class="card-title"></h3><p class="card-summary"></p>`;
|
||||||
card.querySelector('.card-bundle').textContent = s.plugin;
|
card.querySelector('.card-bundle').textContent = s.plugin;
|
||||||
|
const tierEl = card.querySelector('.card-tier');
|
||||||
|
tierEl.textContent = `${meta.dot} ${meta.label}`;
|
||||||
|
tierEl.classList.add(meta.cls);
|
||||||
card.querySelector('.card-title').textContent = s.title;
|
card.querySelector('.card-title').textContent = s.title;
|
||||||
card.querySelector('.card-summary').textContent = s.summary || s.description;
|
card.querySelector('.card-summary').textContent = s.summary || s.description;
|
||||||
card.addEventListener('click', () => selectSkill(s));
|
card.addEventListener('click', () => selectSkill(s));
|
||||||
@@ -104,8 +123,14 @@ function selectSkill(s) {
|
|||||||
el('controls').hidden = true;
|
el('controls').hidden = true;
|
||||||
el('runner').hidden = false;
|
el('runner').hidden = false;
|
||||||
el('skillBundle').textContent = s.plugin;
|
el('skillBundle').textContent = s.plugin;
|
||||||
|
const meta = TIER_META[s.tier] || TIER_META.stable;
|
||||||
|
const tierTag = el('skillTier');
|
||||||
|
tierTag.textContent = `${meta.dot} ${meta.label}`;
|
||||||
|
tierTag.className = 'tier-tag ' + meta.cls;
|
||||||
el('skillTitle').textContent = s.title;
|
el('skillTitle').textContent = s.title;
|
||||||
el('skillDesc').textContent = s.description;
|
el('skillDesc').textContent = s.description;
|
||||||
|
el('elsewhere').open = false;
|
||||||
|
el('copyMsg').textContent = '';
|
||||||
el('outputWrap').hidden = true;
|
el('outputWrap').hidden = true;
|
||||||
el('output').innerHTML = '';
|
el('output').innerHTML = '';
|
||||||
setStatus('');
|
setStatus('');
|
||||||
@@ -225,6 +250,29 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format the current skill's instructions for another assistant, mirroring the
|
||||||
|
// per-platform renderers in scripts/build-exports.mjs.
|
||||||
|
function promptFor(platform) {
|
||||||
|
if (!current) return '';
|
||||||
|
const body = current.instructions;
|
||||||
|
if (platform === 'gemini') {
|
||||||
|
return `You are a specialised assistant. ${current.description}\n\nFollow these instructions:\n\n${body}`;
|
||||||
|
}
|
||||||
|
return body; // chatgpt + claude use the body directly
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyPrompt(platform) {
|
||||||
|
const text = promptFor(platform);
|
||||||
|
if (!text) return;
|
||||||
|
const labels = { chatgpt: 'ChatGPT', gemini: 'Gemini', claude: 'raw' };
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
el('copyMsg').textContent = `Copied ${labels[platform]} prompt — paste it into the tool's instructions.`;
|
||||||
|
} catch (_) {
|
||||||
|
el('copyMsg').textContent = 'Copy failed — your browser blocked clipboard access.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildUserMessage(fields) {
|
function buildUserMessage(fields) {
|
||||||
return fields
|
return fields
|
||||||
.filter((f) => f.value.trim())
|
.filter((f) => f.value.trim())
|
||||||
|
|||||||
+19
-2
@@ -10,6 +10,16 @@ const root = join(__dirname, '..');
|
|||||||
const skillsDir = join(root, 'skills');
|
const skillsDir = join(root, 'skills');
|
||||||
const pluginsDir = join(root, 'plugins');
|
const pluginsDir = join(root, 'plugins');
|
||||||
|
|
||||||
|
// --- Skill tiers (single source: skill-tiers.json) ---
|
||||||
|
// Anything not listed is 'stable'. Used to badge/filter skills in the playground.
|
||||||
|
let TIERS = { productionReady: [], experimental: [] };
|
||||||
|
const tiersFile = join(root, 'skill-tiers.json');
|
||||||
|
if (existsSync(tiersFile)) TIERS = { ...TIERS, ...JSON.parse(readFileSync(tiersFile, 'utf8')) };
|
||||||
|
const productionSet = new Set(TIERS.productionReady);
|
||||||
|
const experimentalSet = new Set(TIERS.experimental);
|
||||||
|
const tierFor = (name) =>
|
||||||
|
productionSet.has(name) ? 'production' : experimentalSet.has(name) ? 'experimental' : 'stable';
|
||||||
|
|
||||||
// --- Map each skill name -> plugin bundle (for grouping/filtering) ---
|
// --- Map each skill name -> plugin bundle (for grouping/filtering) ---
|
||||||
const skillToPlugin = {};
|
const skillToPlugin = {};
|
||||||
if (existsSync(pluginsDir)) {
|
if (existsSync(pluginsDir)) {
|
||||||
@@ -98,12 +108,19 @@ for (const name of readdirSync(skillsDir)) {
|
|||||||
description: meta.description || '',
|
description: meta.description || '',
|
||||||
summary: summarize(meta.description || ''),
|
summary: summarize(meta.description || ''),
|
||||||
plugin: skillToPlugin[name] || 'other',
|
plugin: skillToPlugin[name] || 'other',
|
||||||
|
tier: tierFor(name),
|
||||||
inputs: parseInputs(body),
|
inputs: parseInputs(body),
|
||||||
instructions: body.trim(),
|
instructions: body.trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
skills.sort((a, b) => a.title.localeCompare(b.title));
|
skills.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
const out = { generatedAt: new Date().toISOString(), count: skills.length, skills };
|
// No wall-clock timestamp: the output must be deterministic so CI can verify it
|
||||||
|
// is in sync with the source skills (a timestamp would make every build differ).
|
||||||
|
const out = { count: skills.length, skills };
|
||||||
writeFileSync(join(__dirname, 'skills.json'), JSON.stringify(out));
|
writeFileSync(join(__dirname, 'skills.json'), JSON.stringify(out));
|
||||||
console.log(`Wrote web/skills.json — ${skills.length} skills, ${Object.keys(skillToPlugin).length ? new Set(skills.map(s=>s.plugin)).size : 0} bundles.`);
|
const tierCounts = skills.reduce((a, s) => ((a[s.tier] = (a[s.tier] || 0) + 1), a), {});
|
||||||
|
console.log(
|
||||||
|
`Wrote web/skills.json — ${skills.length} skills, ${new Set(skills.map((s) => s.plugin)).size} bundles ` +
|
||||||
|
`(production: ${tierCounts.production || 0}, stable: ${tierCounts.stable || 0}, experimental: ${tierCounts.experimental || 0}).`
|
||||||
|
);
|
||||||
|
|||||||
@@ -39,6 +39,12 @@
|
|||||||
<div class="controls" id="controls">
|
<div class="controls" id="controls">
|
||||||
<input id="search" type="search" placeholder="Search skills…" />
|
<input id="search" type="search" placeholder="Search skills…" />
|
||||||
<select id="pluginFilter"><option value="">All bundles</option></select>
|
<select id="pluginFilter"><option value="">All bundles</option></select>
|
||||||
|
<select id="tierFilter" title="Maturity tier">
|
||||||
|
<option value="">All tiers</option>
|
||||||
|
<option value="production">🟢 Production-Ready</option>
|
||||||
|
<option value="stable">🔵 Stable</option>
|
||||||
|
<option value="experimental">🟡 Experimental</option>
|
||||||
|
</select>
|
||||||
<span id="count" class="count"></span>
|
<span id="count" class="count"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -52,10 +58,22 @@
|
|||||||
|
|
||||||
<div class="skill-head">
|
<div class="skill-head">
|
||||||
<span class="bundle-tag" id="skillBundle"></span>
|
<span class="bundle-tag" id="skillBundle"></span>
|
||||||
|
<span class="tier-tag" id="skillTier"></span>
|
||||||
<h2 id="skillTitle"></h2>
|
<h2 id="skillTitle"></h2>
|
||||||
<p id="skillDesc" class="skill-desc"></p>
|
<p id="skillDesc" class="skill-desc"></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details class="elsewhere" id="elsewhere">
|
||||||
|
<summary>Use this skill in another tool ↗</summary>
|
||||||
|
<p class="elsewhere-note">These skills aren't locked to Claude. Copy the ready-made instructions into another assistant — you keep the full framework, you just paste it once.</p>
|
||||||
|
<div class="elsewhere-actions">
|
||||||
|
<button id="copyChatgpt" class="ghost" type="button">Copy for ChatGPT</button>
|
||||||
|
<button id="copyGemini" class="ghost" type="button">Copy for Gemini</button>
|
||||||
|
<button id="copyClaude" class="ghost" type="button">Copy raw instructions</button>
|
||||||
|
<span id="copyMsg" class="copy-msg"></span>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<form id="inputForm" class="input-form"></form>
|
<form id="inputForm" class="input-form"></form>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -93,6 +93,17 @@ a { color: var(--accent-2); }
|
|||||||
.card-summary { margin: 0; font-size: 12.5px; color: var(--muted); line-height: 1.5; }
|
.card-summary { margin: 0; font-size: 12.5px; color: var(--muted); line-height: 1.5; }
|
||||||
.empty-msg { color: var(--muted); padding: 40px; text-align: center; grid-column: 1 / -1; }
|
.empty-msg { color: var(--muted); padding: 40px; text-align: center; grid-column: 1 / -1; }
|
||||||
|
|
||||||
|
/* ---------- Tier badges ---------- */
|
||||||
|
.card-tags { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.card-tags .card-bundle { margin: 0; }
|
||||||
|
.card-tier {
|
||||||
|
font-size: 10px; font-weight: 600; letter-spacing: .02em; padding: 2px 7px;
|
||||||
|
border-radius: 99px; border: 1px solid transparent; white-space: nowrap; margin-left: auto;
|
||||||
|
}
|
||||||
|
.tier-production { color: #6ee7b7; background: rgba(16,185,129,.12); border-color: rgba(16,185,129,.35); }
|
||||||
|
.tier-stable { color: #93c5fd; background: rgba(59,130,246,.12); border-color: rgba(59,130,246,.35); }
|
||||||
|
.tier-experimental { color: #fcd34d; background: rgba(245,158,11,.12); border-color: rgba(245,158,11,.35); }
|
||||||
|
|
||||||
/* ---------- Runner ---------- */
|
/* ---------- Runner ---------- */
|
||||||
.runner { max-width: 880px; margin: 0 auto; }
|
.runner { max-width: 880px; margin: 0 auto; }
|
||||||
.back {
|
.back {
|
||||||
@@ -108,6 +119,22 @@ a { color: var(--accent-2); }
|
|||||||
}
|
}
|
||||||
.skill-head h2 { margin: 0 0 6px; font-size: 23px; }
|
.skill-head h2 { margin: 0 0 6px; font-size: 23px; }
|
||||||
.skill-desc { color: var(--muted); font-size: 13.5px; line-height: 1.55; margin: 0; }
|
.skill-desc { color: var(--muted); font-size: 13.5px; line-height: 1.55; margin: 0; }
|
||||||
|
.tier-tag {
|
||||||
|
display: inline-block; font-size: 11px; font-weight: 600; padding: 3px 9px;
|
||||||
|
border-radius: 99px; margin: 0 0 8px 6px; border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Use in another tool ---------- */
|
||||||
|
.elsewhere {
|
||||||
|
background: var(--panel); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; padding: 4px 14px; margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.elsewhere summary {
|
||||||
|
cursor: pointer; font-size: 13px; font-weight: 600; padding: 9px 0; color: var(--accent-2);
|
||||||
|
}
|
||||||
|
.elsewhere-note { color: var(--muted); font-size: 12.5px; line-height: 1.5; margin: 4px 0 12px; }
|
||||||
|
.elsewhere-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-bottom: 12px; }
|
||||||
|
.copy-msg { font-size: 12px; color: #6ee7b7; }
|
||||||
|
|
||||||
.input-form { display: flex; flex-direction: column; gap: 14px; margin: 22px 0; }
|
.input-form { display: flex; flex-direction: column; gap: 14px; margin: 22px 0; }
|
||||||
.field label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 5px; }
|
.field label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 5px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user