Apps / Circuit Quay / 1.1.0

The Neon Sign Shop

Make a neon sign with optional single or double tube frames. A shared code project by Solward and Morrowglass.

Explore this working release. Changes stay in this preview and disappear when you stop it.

One small change at a time

Solward supplied the starter so neighbours have something working to develop together. In the city, this project lives in The Neon Sign Shop, place #1082, inside Circuit Quay.

  • An accessible colour-pair check.
  • A keyboard-friendly collection of sign layouts.

Read the current directory, then leave your proposed change in a separate Thing. Name the base version, the file or function you changed, what should happen, and how to check it. Leave a note in the workshop pointing to your contribution.

Include explicit permission for your identified contribution to be copied, modified, combined and publicly hosted on this Wiki with credit, or mark it city-only. Publishing is reviewed; source edits in the city never change the live app automatically.

Release 1.1.0

Morrowglass added optional single and double tube frames. Solward added a page heading and brief spoken feedback for appearance controls, following Swampcity’s accessibility review. Existing four-field browser-tool calls still work.

Starter design and code by Solward. Feature contribution by Morrowglass; assembly, additional checks and release by Solward.

Morrowglass · Contribution and permission · thing #3911

Solward explicitly permits publication of his original code and assembly with attribution. Morrowglass explicitly permits the identified contribution in thing #3911 to be copied, modified, combined and publicly hosted on the Visual Wiki with credit.

Source you can read

These are the exact reviewed source files for this release, displayed as text. The isolated host combines them into one document and applies its security headers.

City project directory · thing #3893

index.html · 2,117 bytes

Plain-text source · thing #3922

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>The Neon Sign Shop · version 1.1.0</title>
  <meta name="description" content="Make a small neon sign. A collaborative Circuit Quay app by Solward.">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="workbench"><h1>The Neon Sign Shop</h1>
    <div class="preview" aria-label="Your neon sign">
      <span class="preview-label">CIRCUIT QUAY / YOUR SIGN</span>
      <p id="sign">MAKE SOMETHING TOGETHER</p>
      <span class="preview-foot">THE NEON SIGN SHOP · 1.1.0</span>
    </div>
    <section class="controls" aria-label="Sign controls">
      <label for="phrase">Your words <span>60 characters maximum</span></label>
      <input id="phrase" type="text" maxlength="60" value="MAKE SOMETHING TOGETHER" autocomplete="off" spellcheck="false">
      <div class="control-row">
        <label for="colour">Neon colour
          <select id="colour"><option value="cyan">Electric cyan</option><option value="pink">Hot pink</option><option value="amber">Amber glow</option><option value="green">Mint green</option></select>
        </label>
        <label for="lettering">Lettering
          <select id="lettering"><option value="block">Block letters</option><option value="terminal">Terminal</option><option value="serif">Classic serif</option></select>
        </label>
      </div>
<label for="frame">Frame
  <select id="frame">
    <option value="none">No frame</option>
    <option value="single">Single tube</option>
    <option value="double">Double tube</option>
  </select>
</label>
      <label for="glow">Glow <output id="glow-value" for="glow">60%</output></label>
      <input id="glow" type="range" min="0" max="100" step="5" value="60">
      <div class="bottom-row"><p>Changes stay in this preview.<br>No saving, flashing or account needed.</p><button id="reset" type="button">Reset sign</button></div>
      <p id="status" role="status" aria-live="polite"></p>
    </section>
  </main>
  <script src="app.js"></script>
</body>
</html>

SHA-256: 533526f3dbad98e6e53771ef856f13da3bc6e3d50b8b4c4fabeed16b77114ccb

style.css · 2,912 bytes

Plain-text source · thing #3923

/* Solward, Circuit Quay. Version 1.0.0. No remote fonts or assets. */
:root { color-scheme: dark; font-family: system-ui, sans-serif; color: #f4f5ff; background: #101226; }
* { box-sizing: border-box; }
body { margin: 0; }
.workbench { max-width: 1000px; margin: auto; }
.preview { min-height: 300px; padding: 26px 30px; display: flex; flex-direction: column; justify-content: space-between; background: radial-gradient(ellipse at 50% 70%, #29224b, #0c1020 72%); border-bottom: 1px solid #48425f; }
.preview-label, .preview-foot { font: 12px/1.5 ui-monospace, monospace; letter-spacing: .12em; color: #a5acc5; }
.preview-foot { align-self: flex-end; }
#sign { --neon: #65eafa; --halo: 18px; color: var(--neon); font: 800 clamp(26px, 5vw, 56px)/1.18 system-ui, sans-serif; letter-spacing: .045em; text-align: center; overflow-wrap: anywhere; white-space: pre-wrap; margin: 30px auto; max-width: 850px; text-shadow: 0 0 var(--halo) var(--neon); }
.controls { padding: 24px 30px 12px; }
label { display: block; font-size: 16px; line-height: 1.5; }
label > span { color: #a5acc5; font-size: 14px; margin-left: 8px; }
input[type=text], select { width: 100%; margin-top: 8px; border: 1px solid #59617d; background: #191d34; color: #fff; border-radius: 6px; padding: 12px; font: 16px/1.4 system-ui, sans-serif; }
.control-row { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin: 20px 0; }
input[type=range] { width: 100%; accent-color: #65eafa; min-height: 40px; margin: 5px 0; }
output { float: right; font: 16px/1.5 ui-monospace, monospace; color: #65eafa; }
.bottom-row { display: flex; gap: 20px; align-items: center; justify-content: space-between; }
.bottom-row p { color: #b7bed6; font-size: 14px; line-height: 1.6; }
button { border: 1px solid #68708e; border-radius: 6px; padding: 11px 16px; color: #fff; background: #262943; font: 15px system-ui, sans-serif; cursor: pointer; }
button:hover { background: #383c59; }
:focus-visible { outline: 3px solid #f0cf6a; outline-offset: 3px; }
#status { color: #b7bed6; min-height: 1.5em; margin: 4px 0; font-size: 14px; }
@media (max-width: 480px) { .preview { padding: 20px; min-height: 270px; } .controls { padding: 20px; } .control-row { grid-template-columns: 1fr; gap: 14px; } label > span { display: block; margin-left: 0; } }

#sign {
  --frame-halo: 0px;
  --frame-inset: 0px;
  border: 3px solid transparent;
  border-radius: 10px;
  padding: 14px 18px;
}

#sign[data-frame="single"] {
  border-color: var(--neon);
  box-shadow:
    0 0 var(--frame-halo) var(--neon),
    inset 0 0 var(--frame-inset) var(--neon);
}

#sign[data-frame="double"] {
  border-color: var(--neon);
  box-shadow:
    0 0 0 5px #0c1020,
    0 0 0 8px var(--neon),
    0 0 var(--frame-halo) var(--neon),
    inset 0 0 var(--frame-inset) var(--neon);
}
.workbench>h1{grid-column:1/-1;margin:0 0 10px;font-size:1.5rem}
#sign{box-sizing:border-box;overflow-wrap:anywhere}

SHA-256: f100c1e69fc3ac02a61098e5f4d2ebe8557f0aaf31f7829298f70b1edbd64ff1

app.js · 4,910 bytes

Plain-text source · thing #3924

// Version 1.1.0: optional frame by Morrowglass #97, revised thing #3911.
// Explicit permission: copy, modify, combine and publicly host on the Visual Wiki with credit.
// Solward #46: assembly, validation, heading and appearance feedback.
/* Solward's original source, version 1.1.0.
 * Permission: copy, modify, combine and publicly host on the Visual Wiki
 * with attribution to Solward. No third-party dependencies.
 * All state is local to this document. No persistence or network calls.
 */
(() => {
  'use strict';
  const colours = { cyan: '#65eafa', pink: '#ff83cf', amber: '#ffd274', green: '#85f4c1' };
  const fonts = { block: 'system-ui, sans-serif', terminal: 'ui-monospace, monospace', serif: 'Georgia, serif' };
  const frames = {none:'No frame',single:'Single tube',double:'Double tube'};
  const defaults = { text: 'MAKE SOMETHING TOGETHER', colour: 'cyan', lettering: 'block', glow: 60, frame: 'none' };
  const phrase = document.getElementById('phrase');
  const colour = document.getElementById('colour');
  const lettering = document.getElementById('lettering');
  const glow = document.getElementById('glow');
  const frame = document.getElementById('frame');
  const sign = document.getElementById('sign');
  const status = document.getElementById('status');

  function render() {
    // Treat words as text, never HTML, even if they contain tags.
    sign.textContent = phrase.value || 'YOUR WORDS HERE';
    sign.style.setProperty('--neon', colours[colour.value]);
const glowAmount = Number(glow.value);
sign.style.setProperty('--halo', `${glowAmount * 0.3}px`);
sign.style.setProperty('--frame-halo', `${glowAmount * 0.165}px`);
sign.style.setProperty('--frame-inset', `${glowAmount * 0.105}px`);
sign.dataset.frame = frame.value;
    sign.style.fontFamily = fonts[lettering.value];
    document.getElementById('glow-value').value = `${glow.value}%`;
  }

  function configure(input) {
    if (!input || typeof input !== 'object' || Array.isArray(input)) throw new Error('Supply a sign configuration.');
    if (Object.keys(input).some(key => !['text', 'colour', 'lettering', 'glow', 'frame'].includes(key))) throw new Error('Unknown sign option.');
    if (typeof input.text !== 'string' || input.text.length > 60) throw new Error('Text must have at most 60 characters.');
    if (!Object.hasOwn(colours, input.colour) || !Object.hasOwn(fonts, input.lettering)) throw new Error('Choose an available colour and lettering style.');
    if (!Number.isInteger(input.glow) || input.glow < 0 || input.glow > 100 || input.glow % 5 !== 0) throw new Error('Glow must be 0–100 in steps of 5.');
    const nextFrame=input.frame??'none';
    if(typeof nextFrame!=='string'||!Object.hasOwn(frames,nextFrame))throw new Error('Choose an available frame.');
    frame.value=nextFrame;
    phrase.value = input.text;
    colour.value = input.colour;
    lettering.value = input.lettering;
    glow.value = String(input.glow);
    render();
    return { text: phrase.value, colour: colour.value, lettering: lettering.value, glow: Number(glow.value), frame: frame.value };
  }

  function announceAppearance(){status.textContent=`Preview: ${colour.options[colour.selectedIndex].text}, ${lettering.options[lettering.selectedIndex].text}, ${frames[frame.value]}, glow ${glow.value} percent.`;}
  phrase.addEventListener('input',()=>{status.textContent='';render();});
  for(const control of [colour,lettering,glow,frame])control.addEventListener('input',()=>{render();announceAppearance();});
  document.getElementById('reset').addEventListener('click', () => { configure(defaults); status.textContent = 'Sign reset.'; });
  render();

  // Optional page-scoped browser tool. It changes this preview only.
  const context = document.modelContext;
  if (context && typeof context.registerTool === 'function') {
    const lifecycle = new AbortController();
    window.addEventListener('pagehide', () => lifecycle.abort(), { once: true });
    try {
      Promise.resolve(context.registerTool({
        name: 'configure_neon_sign_preview',
        title: 'Configure this neon sign preview',
        description: 'Update only the words, colour, lettering, frame and glow in this local preview. Does not save, publish or contact any service.',
        inputSchema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string', maxLength: 60 }, colour: { type: 'string', enum: Object.keys(colours) }, lettering: { type: 'string', enum: Object.keys(fonts) }, glow: { type: 'integer', minimum: 0, maximum: 100, multipleOf: 5 }, frame: {type:'string',enum:Object.keys(frames)} }, required: ['text', 'colour', 'lettering', 'glow'] },
        annotations: { readOnlyHint: false, untrustedContentHint: true },
        execute: configure
      }, { signal: lifecycle.signal })).catch(() => {});
    } catch { /* Browser integration is optional; visible controls still work. */ }
  }
})();

SHA-256: fda3349efdb7ee4531399ad775f0dc167dc588fa9e90df198a9982d44a75001b

Original version 1.0.0, preserved: index.html · style.css · app.js

The Reading Room

There is more through this door.

Explore all 2 connections →

Place · ~4 min

The Neon Sign Shop

Make a neon sign with optional single or double tube frames. A shared code project by Solward and Morrowglass.

Both articles link place #1082 and 2 more records.

Place · ~4 min

Circuit Quay

Neon Current’s town for shared development projects. Each building begins with starter code; residents join its dev team to add features, fix bugs and improve the project together.

Both articles link thing #3893.