twiddle-panel 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/bin/init.js +118 -0
- package/dist/twiddle.js +60 -45
- package/dist/twiddle.mjs +1122 -1037
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -12,6 +12,14 @@ Panel is a design tool: load it in development or in shared prototype links, not
|
|
|
12
12
|
|
|
13
13
|
Panel used to be published as `@theoldpixel/twiddle`; that package is deprecated in favour of `twiddle-panel`.
|
|
14
14
|
|
|
15
|
+
**Quickest: let the setup command find the right file**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx twiddle-panel init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
It shows the line to add and where (your entry file, or `index.html` for plain pages), and changes nothing. Add `--write` to add it, `--dev-only` to load the panel only while developing. For Next.js it prints a small client component to use instead.
|
|
22
|
+
|
|
15
23
|
**Projects with a build tool (Vite, Next.js…)**
|
|
16
24
|
|
|
17
25
|
```bash
|
|
@@ -35,7 +43,7 @@ if (import.meta.env.DEV) import('twiddle-panel');
|
|
|
35
43
|
**Plain HTML prototypes** (no install needed)
|
|
36
44
|
|
|
37
45
|
```html
|
|
38
|
-
<script src="https://cdn.jsdelivr.net/npm/twiddle-panel@0.
|
|
46
|
+
<script src="https://cdn.jsdelivr.net/npm/twiddle-panel@0.8.0/dist/twiddle.js" defer></script>
|
|
39
47
|
```
|
|
40
48
|
|
|
41
49
|
The prototype's CSS must use the `--twd-*` token variables
|
|
@@ -49,14 +57,18 @@ Optional component tokens (contract v0.4) let the Shape tab restyle the main but
|
|
|
49
57
|
Optional layout roles (token contract v0.2) name what a space is for:
|
|
50
58
|
`--twd-page-pad`, `--twd-section-gap`, `--twd-card-pad`, `--twd-card-gap` and
|
|
51
59
|
`--twd-control-gap`. Each points at a spacing step by default, so prototypes
|
|
52
|
-
that use them can be re-pointed in **
|
|
60
|
+
that use them can be re-pointed in **Spacing › Layout roles**.
|
|
53
61
|
|
|
54
62
|
## Using the panel
|
|
55
63
|
|
|
64
|
+
Open it with the Panel button in the bottom-right corner, or Alt+T (Option+T on a Mac). The panel opens to the left of the button.
|
|
65
|
+
|
|
56
66
|
- **Tabs** along the bottom: **Overview** (the **Character** pad: up/down for spacing, Subtle ↔ Bold for weights, borders, corners and shadows; plus a summary of the current look), **Colour** (four themes, then fine-tune with the Tailwind palette: a colour family and shade for the primary, a grey family for the neutrals, or any custom hex; Light/Dark), **Type** (font pairings as tiles, base size, scale), **Spacing** (density, layout roles), **Shape** (button style, corners and size; card style, corners and borders) and **Shadows**. Each tab ends with its tokens as rows you can set to a fixed value; an edited token shows **Custom** and its own reset. The header holds the global actions; **Copy link** shares the exact version (`?twd=…`).
|
|
57
67
|
- **Pick** (crosshair) selects one element. The panel then shows only the sections that element uses. Choose **This element** or **All `.class` (n)** to set where the tweaks go. Add `data-twd="name"` to an element to control what counts as "all like this".
|
|
58
68
|
- **Compare** (half circle): hold it, or press Alt+C, to see the original page.
|
|
59
|
-
- **
|
|
69
|
+
- **Minimise** (line) shrinks the panel to just its tabs, beside the Panel button; click a tab to open it again.
|
|
70
|
+
- **Move it**: drag the Panel button to any spot; it attaches to the nearest corner, keeps its distance from the edges, and the panel opens on the side away from the edge. With the button focused, arrow keys nudge it 1px (Shift: 10px). Double-click it to put it back bottom-right. Remembered for the site in this browser.
|
|
71
|
+
- **Undo / Redo** in the header, or ⌘Z / ⇧⌘Z while the panel has focus. Reset can be undone too.
|
|
60
72
|
- **Changes** lists every tweak with the token values before and after. Use × to remove one.
|
|
61
73
|
- **Copy prompt** turns the tweaks into a Markdown prompt for a coding agent, so they become permanent in the source. Use the arrow next to it to choose between *Preview before copying* and *Copy without preview*.
|
|
62
74
|
- Colour pairs that fall below WCAG AA show a warning in Colour, in Changes and in the prompt.
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ---------------------------------------------------------
|
|
3
|
+
npx twiddle-panel init — optional setup helper (TODO: "Later").
|
|
4
|
+
|
|
5
|
+
Finds where Panel should load and shows the line to add.
|
|
6
|
+
Changes nothing unless --write is passed; never adds a line twice.
|
|
7
|
+
|
|
8
|
+
npx twiddle-panel init show what to add, and where
|
|
9
|
+
npx twiddle-panel init --write add it
|
|
10
|
+
npx twiddle-panel init --dev-only build tools: load the panel only
|
|
11
|
+
in development (tokens always)
|
|
12
|
+
npx twiddle-panel init --file <path> use this entry / HTML file
|
|
13
|
+
--------------------------------------------------------- */
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join, relative } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const pkgSelf = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
19
|
+
const NAME = pkgSelf.name;
|
|
20
|
+
const VERSION = pkgSelf.version;
|
|
21
|
+
const SCRIPT_TAG = `<script src="https://cdn.jsdelivr.net/npm/${NAME}@${VERSION}/dist/twiddle.js" defer></script>`;
|
|
22
|
+
const IMPORT = `import '${NAME}';`;
|
|
23
|
+
const IMPORT_DEV = `import '${NAME}/tokens.css';\nif (import.meta.env.DEV) import('${NAME}');`;
|
|
24
|
+
const DOCS = 'https://panel.twiddle.in';
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const cmd = args[0];
|
|
28
|
+
const flag = (f) => args.includes(f);
|
|
29
|
+
const fileArg = args.includes('--file') ? args[args.indexOf('--file') + 1] : null;
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
const rel = (p) => relative(cwd, p) || p;
|
|
32
|
+
|
|
33
|
+
const say = (s = '') => process.stdout.write(`${s}\n`);
|
|
34
|
+
const indent = (s) => s.split('\n').map((l) => ` ${l}`).join('\n');
|
|
35
|
+
|
|
36
|
+
if (cmd !== 'init') {
|
|
37
|
+
say(`Panel by Twiddle ${VERSION}\n\nUsage:\n npx ${NAME} init show the line to add, and where\n npx ${NAME} init --write add it\n npx ${NAME} init --dev-only load the panel only in development\n npx ${NAME} init --file <path> use this entry or HTML file\n\nGuide: ${DOCS}`);
|
|
38
|
+
process.exit(cmd && !['help', '--help', '-h'].includes(cmd) ? 1 : 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const readJSON = (p) => { try { return JSON.parse(readFileSync(p, 'utf8')); } catch (e) { return null; } };
|
|
42
|
+
const pkg = readJSON(join(cwd, 'package.json'));
|
|
43
|
+
const deps = pkg ? { ...pkg.dependencies, ...pkg.devDependencies } : {};
|
|
44
|
+
const first = (list) => list.map((p) => join(cwd, p)).find((p) => existsSync(p)) || null;
|
|
45
|
+
|
|
46
|
+
/** The JS file a Vite-style index.html loads (<script type="module" src="/src/main.ts">). */
|
|
47
|
+
function moduleEntry(htmlPath) {
|
|
48
|
+
const m = /<script[^>]*type=["']module["'][^>]*src=["']\/?([^"']+)["']/i.exec(readFileSync(htmlPath, 'utf8'))
|
|
49
|
+
|| /<script[^>]*src=["']\/?([^"']+)["'][^>]*type=["']module["']/i.exec(readFileSync(htmlPath, 'utf8'));
|
|
50
|
+
if (!m || /^https?:/.test(m[1])) return null;
|
|
51
|
+
const p = join(cwd, m[1]);
|
|
52
|
+
return existsSync(p) ? p : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- Work out the kind of project and the file to change ----
|
|
56
|
+
let plan;
|
|
57
|
+
if (deps.next) {
|
|
58
|
+
plan = { kind: 'next' };
|
|
59
|
+
} else if (fileArg) {
|
|
60
|
+
const p = join(cwd, fileArg);
|
|
61
|
+
if (!existsSync(p)) { say(`Can't find ${fileArg}.`); process.exit(1); }
|
|
62
|
+
plan = /\.html?$/i.test(p) ? { kind: 'html', file: p } : { kind: 'build', file: p };
|
|
63
|
+
} else if (pkg) {
|
|
64
|
+
const html = first(['index.html']);
|
|
65
|
+
const file = (html && moduleEntry(html))
|
|
66
|
+
|| first(['src/main.ts', 'src/main.tsx', 'src/main.js', 'src/main.jsx', 'src/index.ts', 'src/index.tsx', 'src/index.js', 'src/index.jsx', 'main.js', 'index.js']);
|
|
67
|
+
plan = file ? { kind: 'build', file } : (html ? { kind: 'html', file: html } : { kind: 'unknown' });
|
|
68
|
+
} else {
|
|
69
|
+
const html = first(['index.html']);
|
|
70
|
+
plan = html ? { kind: 'html', file: html } : { kind: 'unknown' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- Next.js: the panel needs the browser, so only print guidance ----
|
|
74
|
+
if (plan.kind === 'next') {
|
|
75
|
+
say(`This looks like a Next.js project. Panel needs the browser, so load it from a client component:\n`);
|
|
76
|
+
say(indent(`// app/twiddle.tsx\n'use client';\nimport { useEffect } from 'react';\nimport '${NAME}/tokens.css';\nexport default function Twiddle() {\n useEffect(() => { import('${NAME}'); }, []);\n return null;\n}`));
|
|
77
|
+
say(`\nThen render <Twiddle /> once in app/layout.tsx, inside <body>.`);
|
|
78
|
+
if (!deps[NAME]) say(`\nInstall first:\n npm install -D ${NAME}`);
|
|
79
|
+
say(`\nGuide: ${DOCS}`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (plan.kind === 'unknown') {
|
|
84
|
+
say(`Couldn't find an entry file or index.html here.\n\nWith a build tool, add this to your app's entry file:\n${indent(IMPORT)}\n\nIn a plain HTML page, add this inside <head>:\n${indent(SCRIPT_TAG)}\n\nOr point at a file: npx ${NAME} init --file <path>\nGuide: ${DOCS}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- Show, then (with --write) add ----
|
|
89
|
+
const src = readFileSync(plan.file, 'utf8');
|
|
90
|
+
const lines = plan.kind === 'html' ? SCRIPT_TAG : (flag('--dev-only') ? IMPORT_DEV : IMPORT);
|
|
91
|
+
const where = plan.kind === 'html' ? `inside <head> of ${rel(plan.file)}` : `at the top of ${rel(plan.file)}`;
|
|
92
|
+
|
|
93
|
+
if (src.includes(NAME) || src.includes('twiddle.js')) {
|
|
94
|
+
say(`Panel is already loaded in ${rel(plan.file)}. Nothing to do.`);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!flag('--write')) {
|
|
99
|
+
say(`Add this ${where}:\n\n${indent(lines)}\n`);
|
|
100
|
+
if (plan.kind === 'build' && !deps[NAME]) say(`Install first:\n npm install -D ${NAME}\n`);
|
|
101
|
+
say(`Run again with --write to add it for you.${plan.kind === 'build' && !flag('--dev-only') ? ' Add --dev-only to load the panel only while developing.' : ''}\nGuide: ${DOCS}`);
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let out;
|
|
106
|
+
if (plan.kind === 'html') {
|
|
107
|
+
if (!/<\/head>/i.test(src)) { say(`${rel(plan.file)} has no </head>. Add this inside <head> yourself:\n${indent(SCRIPT_TAG)}`); process.exit(1); }
|
|
108
|
+
out = src.replace(/(\s*)<\/head>/i, (m, ws) => `${ws} ${SCRIPT_TAG}${ws}</head>`);
|
|
109
|
+
} else {
|
|
110
|
+
// At the top, after a leading 'use client'-style directive if there is one.
|
|
111
|
+
const directive = /^(\s*(['"])use [a-z]+\2;?\s*\n)/.exec(src);
|
|
112
|
+
const at = directive ? directive[0].length : 0;
|
|
113
|
+
out = `${src.slice(0, at)}${lines}\n${src.slice(at)}`;
|
|
114
|
+
}
|
|
115
|
+
writeFileSync(plan.file, out);
|
|
116
|
+
say(`Added Panel ${where}.`);
|
|
117
|
+
if (plan.kind === 'build' && !deps[NAME]) say(`\nNow install it:\n npm install -D ${NAME}`);
|
|
118
|
+
say(`\nYour CSS needs the --twd-* tokens for the panel to change anything. Guide and agent prompt: ${DOCS}`);
|