pishposh
Version:
Visual Programming Language
395 lines (367 loc) • 15.2 kB
HTML
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍥</text></svg>">
<title>Package.json Editor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css" />
<script type="importmap">
{
"imports": {}
}
</script>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js" integrity="sha384-j1CDi7MgGQ12Z7Qab0qlWQ/Qqz24Gc6BM0thvEMVjHnfYGF0rmFCozFSxQBxwHKO" crossorigin="anonymous"></script>
<script type="module">
import { html, Signal } from 'https://cdn.jsdelivr.net/npm/luminiferous';
// Helper function for random selection
function oneOf(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}
// Package data as signals
const packageData = {
name: new Signal("basic/pass-through"),
version: new Signal("1.2.3"),
description: new Signal("Applies various image filters with real-time preview and adjustable parameters"),
author: {
name: new Signal("John Doe"),
email: new Signal("john@example.com"),
url: new Signal("https://example.com")
},
license: new Signal("MIT"),
repository: {
type: new Signal("git"),
url: new Signal("https://github.com/myorg/image-filter-node.git")
},
keywords: new Signal(["image", "filter", "processing", "graphics"]),
category: new Signal("image-processing"),
subcategory: new Signal("filters")
};
// Visual state
const appState = {
activeTab: new Signal("basic"),
isDirty: new Signal(false),
lastSaved: new Signal(new Date()),
animatedIcon: new Signal("bi-box-seam")
};
// Animate icon
setInterval(() => {
appState.animatedIcon.value = oneOf(['bi-box-seam', 'bi-boxes', 'bi-archive', 'bi-file-earmark-code']);
}, 2000);
// Track changes
Object.values(packageData).forEach(signal => {
if (signal instanceof Signal) {
signal.subscribe(() => appState.isDirty.value = true);
}
});
// Helper function to create form fields
function createField(label, signal, type = "text", options = {}) {
const id = `field-${label.toLowerCase().replace(/\s+/g, '-')}`;
const {placeholder = " ", help = "", required = false, rows = 3} = options;
if (type === "textarea") {
return html`
<div class="mb-3">
<label for="${id}" class="form-label">${label} ${required ? html`<span class="text-danger">*</span>` : ''}</label>
<textarea class="form-control" id="${id}" rows="${rows}" placeholder="${placeholder}" value=${signal} oninput="${(e) => signal.value = e.target.value}">
</textarea>
${help ? html`<div class="form-text">${help}</div>` : ''}
</div>
`;
}
if (type === "select") {
return html`
<div class="mb-3">
<label for="${id}" class="form-label">${label} ${required ? html`<span class="text-danger">*</span>` : ''}</label>
<select class="form-select" id="${id}" value=${signal} oninput="${(e) => signal.value = e.target.value}">
${options.choices.map(choice => html`
<option value="${choice.value}" selected=${signal.value === choice.value ? 'selected' : ''}>${choice.label}</option>
`)}
</select>
${help ? html`<div class="form-text">${help}</div>` : ''}
</div>
`;
}
return html`
<div class="mb-3">
<label for="${id}" class="form-label">${label} ${required ? html`<span class="text-danger">*</span>` : ''}</label>
<input type="${type}" class="form-control" id="${id}" placeholder="${placeholder}" value=${signal} oninput="${(e) => signal.value = e.target.value}">
${help ? html`<div class="form-text">${help}</div>` : ''}
</div>
`;
}
// Keywords manager
function keywordsManager() {
const newKeyword = new Signal("");
return html`
<div class="mb-3">
<label class="form-label">Keywords</label>
<div class="d-flex gap-2 mb-2">
<input type="text" class="form-control" placeholder="Add keyword"
${{
value: newKeyword,
oninput: (e) => newKeyword.value = e.target.value,
onkeydown: (e) => {
if (e.key === 'Enter' && newKeyword.value.trim()) {
packageData.keywords.value = [...packageData.keywords.value, newKeyword.value.trim()];
newKeyword.value = "";
e.preventDefault();
}
}
}}>
<button type="button" class="btn btn-outline-primary"
${{
onclick: () => {
if (newKeyword.value.trim()) {
packageData.keywords.value = [...packageData.keywords.value, newKeyword.value.trim()];
newKeyword.value = "";
}
}
}}>
<i class="bi bi-plus"></i>
</button>
</div>
<div class="d-flex flex-wrap gap-1">
${packageData.keywords.value.map((keyword, index) => html`
<span class="badge bg-secondary d-flex align-items-center gap-1">
${keyword}
<button type="button" class="btn-close btn-close-white"
style="font-size: 0.6em;"
${{
onclick: () => {
packageData.keywords.value = packageData.keywords.value.filter((_, i) => i !== index);
}
}}></button>
</span>
`)}
</div>
</div>
`;
}
// Navigation tabs
function createTabs() {
const tabs = [
{ id: "basic", label: "Basic Info", icon: "bi-info-circle" },
{ id: "author", label: "Author", icon: "bi-person" },
{ id: "repository", label: "Repository", icon: "bi-github" },
{ id: "advanced", label: "Advanced", icon: "bi-gear" }
];
return html`
<ul class="nav nav-tabs mb-4">
${tabs.map(tab => html`
<li class="nav-item">
<button class="${['nav-link', (appState.activeTab.value === tab.id ? 'active' : 'inactive') ]}" foo="foo" onclick=${function(){appState.activeTab.value=tab.id}}>
<i class="${tab.icon}"></i> ${tab.label}
</button>
</li>
`)}
</ul>
`;
}
// Tab content
function createTabContent() {
const licenseOptions = [
{ value: "MIT", label: "MIT" },
{ value: "Apache-2.0", label: "Apache 2.0" },
{ value: "GPL-3.0", label: "GPL 3.0" },
{ value: "BSD-3-Clause", label: "BSD 3-Clause" },
{ value: "ISC", label: "ISC" }
];
switch (appState.activeTab.value) {
case "basic":
return html`
<div class="row">
<div class="col-md-6">
${createField("Package Name", packageData.name, "text", {
placeholder: "e.g., @myorg/package-name",
help: "The name of your package",
required: true
})}
${createField("Version", packageData.version, "text", {
placeholder: "e.g., 1.0.0",
help: "Semantic version number",
required: true
})}
${createField("License", packageData.license, "select", {
choices: licenseOptions,
help: "Choose a license for your package"
})}
</div>
<div class="col-md-6">
${createField("Description", packageData.description, "textarea", {
placeholder: "Brief description of what your package does",
help: "This will appear in package listings",
required: true
})}
${createField("Category", packageData.category, "text", {
placeholder: "e.g., image-processing"
})}
${createField("Subcategory", packageData.subcategory, "text", {
placeholder: "e.g., filters"
})}
</div>
</div>
${keywordsManager()}
`;
case "author":
return html`
<div class="row">
<div class="col-md-6">
${createField("Author Name", packageData.author.name, "text", {
placeholder: "Your full name",
required: true
})}
${createField("Author Email", packageData.author.email, "email", {
placeholder: "your.email@example.com"
})}
</div>
<div class="col-md-6">
${createField("Author URL", packageData.author.url, "url", {
placeholder: "https://yourwebsite.com",
help: "Your personal website or portfolio"
})}
</div>
</div>
`;
case "repository":
return html`
<div class="row">
<div class="col-md-6">
${createField("Repository Type", packageData.repository.type, "select", {
choices: [
{ value: "git", label: "Git" },
{ value: "svn", label: "SVN" },
{ value: "hg", label: "Mercurial" }
]
})}
</div>
<div class="col-md-6">
${createField("Repository URL", packageData.repository.url, "url", {
placeholder: "https://github.com/username/repo.git",
help: "The repository URL where your code is hosted"
})}
</div>
</div>
`;
case "advanced":
return html`
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Advanced configuration options would go here. This could include:
<ul class="mb-0 mt-2">
<li>Engine requirements</li>
<li>Peer dependencies</li>
<li>Build scripts</li>
<li>File configurations</li>
</ul>
</div>
`;
default:
return html`<div>Select a tab to edit package information</div>`;
}
}
// JSON preview
function createJsonPreview() {
const jsonOutput = {
name: packageData.name.value,
version: packageData.version.value,
description: packageData.description.value,
author: {
name: packageData.author.name.value,
email: packageData.author.email.value,
url: packageData.author.url.value
},
license: packageData.license.value,
repository: {
type: packageData.repository.type.value,
url: packageData.repository.url.value
},
keywords: packageData.keywords.value,
category: packageData.category.value,
subcategory: packageData.subcategory.value
};
return html`
<div class="position-sticky" style="top: 2rem;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">
<i class="bi bi-file-earmark-code"></i> package.json Preview
</h5>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary btn-sm"
${{
onclick: () => {
navigator.clipboard.writeText(JSON.stringify(jsonOutput, null, 2));
}
}}>
<i class="bi bi-clipboard"></i> Copy
</button>
<button class="btn btn-primary btn-sm"
${{
onclick: () => {
const blob = new Blob([JSON.stringify(jsonOutput, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'package.json';
a.click();
URL.revokeObjectURL(url);
appState.isDirty.value = false;
appState.lastSaved.value = new Date();
}
}}>
<i class="bi bi-download"></i> Download
</button>
</div>
</div>
<div class="border rounded p-3 bg-body-tertiary">
<pre class="mb-0" style="font-size: 0.8em; max-height: 60vh; overflow-y: auto;">
<code>${JSON.stringify(jsonOutput, null, 2)}</code>
</pre>
</div>
<div class="mt-2 text-muted small">
${appState.isDirty.value ? html`
<i class="bi bi-circle-fill text-warning"></i> Unsaved changes
` : html`
<i class="bi bi-check-circle-fill text-success"></i> Saved ${appState.lastSaved.value.toLocaleTimeString()}
`}
</div>
</div>
`;
}
// Main application
const app = html`
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="display-6">
<i class="${appState.animatedIcon}"></i>
Package.json Editor (<small>${packageData.version}</small>)
</h1>
<div class="badge bg-primary">
Powered by Luminiferous Signals
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-body">
${createTabs()}
${createTabContent()}
</div>
</div>
</div>
<div class="col-lg-4">
${createJsonPreview()}
</div>
</div>
</div>
`;
document.body.appendChild(app);
</script>
</body>
</html>