feat: add Fluid Typography Generator extension for VSCode
- Implemented core functionality to generate fluid typography CSS variables based on user-defined breakpoints and font sizes. - Created configuration options for output format (Tailwind or vanilla CSS) and rounding settings. - Added input parsing for settings from text files or VSCode interface. - Developed CSS generation logic with support for `clamp()` and optional rounding. - Included tests for parsing settings, generating CSS, and inserting text at selection in the editor. - Documented project details and usage in project.md. - Added example CSS output in typography.css.
This commit is contained in:
+390
@@ -0,0 +1,390 @@
|
||||
const vscode = require('vscode');
|
||||
|
||||
const { generateCss } = require('./src/fluidTypography');
|
||||
const { insertTextAtSelectionValue } = require('./src/webviewText');
|
||||
|
||||
const DEFAULT_SIZE_LINES = `text-14px: 12px-14px
|
||||
text-16px: 14px-16px
|
||||
text-18px: 16px-18px
|
||||
text-20px: 18px-20px
|
||||
text-22px: 20px-22px
|
||||
text-25px: 22px-25px
|
||||
text-30px: 25px-30px
|
||||
text-35px: 30px-35px
|
||||
text-38px: 35px-38px
|
||||
text-40px: 38px-40px
|
||||
text-45px: 40px-45px
|
||||
text-50px: 45px-50px
|
||||
text-70px: 50px-70px
|
||||
text-75px: 70px-75px`;
|
||||
|
||||
function getNonce() {
|
||||
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let text = '';
|
||||
|
||||
for (let index = 0; index < 32; index += 1) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
async function openGeneratedCss(css) {
|
||||
const document = await vscode.workspace.openTextDocument({
|
||||
content: css,
|
||||
language: 'css',
|
||||
});
|
||||
|
||||
await vscode.window.showTextDocument(document, { preview: false });
|
||||
}
|
||||
|
||||
function getExtensionSettings() {
|
||||
const config = vscode.workspace.getConfiguration('fluidTypography');
|
||||
|
||||
return {
|
||||
css: config.get('cssMode', 'tailwind'),
|
||||
round: config.get('round.enabled', true),
|
||||
roundValue: config.get('round.value', '2px'),
|
||||
low: config.get('breakpoints.low', 360),
|
||||
high: config.get('breakpoints.high', 1920),
|
||||
};
|
||||
}
|
||||
|
||||
async function generateFromText(input) {
|
||||
try {
|
||||
await openGeneratedCss(generateCss(input, getExtensionSettings()));
|
||||
vscode.window.showInformationMessage('Fluid typography CSS generated.');
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Fluid typography input error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function generateFromFile() {
|
||||
const selection = await vscode.window.showOpenDialog({
|
||||
canSelectFiles: true,
|
||||
canSelectFolders: false,
|
||||
canSelectMany: false,
|
||||
filters: {
|
||||
Text: ['txt', 'md', 'css'],
|
||||
All: ['*'],
|
||||
},
|
||||
title: 'Select fluid typography settings file',
|
||||
});
|
||||
|
||||
if (!selection || selection.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = await vscode.workspace.fs.readFile(selection[0]);
|
||||
await generateFromText(Buffer.from(bytes).toString('utf8'));
|
||||
}
|
||||
|
||||
async function generateFromSelection() {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
|
||||
if (!editor) {
|
||||
vscode.window.showWarningMessage('Open a settings file or select settings text first.');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedText = editor.document.getText(editor.selection);
|
||||
const input = selectedText || editor.document.getText();
|
||||
|
||||
await generateFromText(input);
|
||||
}
|
||||
|
||||
function getWebviewHtml(webview, settings) {
|
||||
const nonce = getNonce();
|
||||
const { cspSource } = webview;
|
||||
const initialState = {
|
||||
...settings,
|
||||
sizes: DEFAULT_SIZE_LINES,
|
||||
};
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src ${cspSource} 'unsafe-inline'; script-src 'nonce-${nonce}';">
|
||||
<title>Fluid Typography Generator</title>
|
||||
<style>
|
||||
body {
|
||||
color: var(--vscode-foreground);
|
||||
font-family: var(--vscode-font-family);
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
background: var(--vscode-input-background);
|
||||
border: 1px solid var(--vscode-input-border, transparent);
|
||||
color: var(--vscode-input-foreground);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
textarea,
|
||||
pre {
|
||||
font-family: var(--vscode-editor-font-family);
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
line-height: 1.5;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 300px;
|
||||
resize: vertical;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--vscode-button-background);
|
||||
border: 0;
|
||||
color: var(--vscode-button-foreground);
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
pre {
|
||||
max-height: 260px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.field-header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="grid">
|
||||
<label for="cssMode">
|
||||
Output
|
||||
<select id="cssMode" title="Tailwind writes variables inside @theme static. Vanilla writes them inside :root.">
|
||||
<option value="tailwind">Tailwind</option>
|
||||
<option value="vanilla">Vanilla CSS</option>
|
||||
</select>
|
||||
<span class="description">Tailwind emits <code>@theme static</code>; vanilla emits <code>:root</code>.</span>
|
||||
</label>
|
||||
<label for="low">
|
||||
Lower breakpoint
|
||||
<input id="low" min="1" step="1" type="number" title="Viewport width where each fluid size reaches its minimum value.">
|
||||
<span class="description">The viewport width where minimum sizes begin.</span>
|
||||
</label>
|
||||
<label for="high">
|
||||
Upper breakpoint
|
||||
<input id="high" min="1" step="1" type="number" title="Viewport width where each fluid size reaches its maximum value.">
|
||||
<span class="description">The viewport width where maximum sizes stop scaling.</span>
|
||||
</label>
|
||||
<label>
|
||||
Rounding
|
||||
<span class="toggle">
|
||||
<input id="round" type="checkbox" title="When enabled, generated values use round(down, clamp(...), value).">
|
||||
<span>Enable CSS round()</span>
|
||||
</span>
|
||||
<span class="description">Rounding can reduce sub-pixel values, but depends on browser support for CSS <code>round()</code>.</span>
|
||||
</label>
|
||||
<label id="roundValueGroup" for="roundValue">
|
||||
Rounding value
|
||||
<input id="roundValue" pattern="\\d+(\\.\\d+)?(px|rem|em)" type="text" title="The interval passed to round(), such as 2px, 0.125rem, or 0.1em.">
|
||||
<span class="description">Used as the final argument in <code>round(down, clamp(...), value)</code>.</span>
|
||||
</label>
|
||||
</div>
|
||||
<label for="sizes">
|
||||
<span class="field-header">
|
||||
<span>Font-size variables</span>
|
||||
<button id="pasteVariables" title="Paste from the VS Code clipboard into this field." type="button">Paste</button>
|
||||
</span>
|
||||
<textarea id="sizes" spellcheck="false" title="One variable per line, in the form name: minpx-maxpx."></textarea>
|
||||
<span class="description">Use one range per line, for example <code>text-18px: 16px-18px</code>.</span>
|
||||
</label>
|
||||
<div class="actions">
|
||||
<button id="preview" type="button">Preview</button>
|
||||
<button id="generate" type="button">Generate CSS Document</button>
|
||||
</div>
|
||||
<pre id="output" aria-live="polite"></pre>
|
||||
</main>
|
||||
<script nonce="${nonce}">
|
||||
const vscode = acquireVsCodeApi();
|
||||
const initialState = ${JSON.stringify(initialState)};
|
||||
const cssMode = document.getElementById('cssMode');
|
||||
const low = document.getElementById('low');
|
||||
const high = document.getElementById('high');
|
||||
const round = document.getElementById('round');
|
||||
const roundValue = document.getElementById('roundValue');
|
||||
const roundValueGroup = document.getElementById('roundValueGroup');
|
||||
const sizes = document.getElementById('sizes');
|
||||
const output = document.getElementById('output');
|
||||
const insertTextAtSelectionValue = ${insertTextAtSelectionValue.toString()};
|
||||
|
||||
function updateRoundVisibility() {
|
||||
roundValueGroup.style.display = round.checked ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
function buildSettingsText() {
|
||||
const lines = [
|
||||
'CSS: ' + cssMode.value,
|
||||
round.checked ? 'Round: yes, ' + roundValue.value : 'Round: no',
|
||||
'Low: ' + low.value + 'px',
|
||||
'High: ' + high.value + 'px',
|
||||
'',
|
||||
sizes.value.trim(),
|
||||
];
|
||||
|
||||
return lines.join('\\n');
|
||||
}
|
||||
|
||||
cssMode.value = initialState.css;
|
||||
low.value = initialState.low;
|
||||
high.value = initialState.high;
|
||||
round.checked = initialState.round;
|
||||
roundValue.value = initialState.roundValue;
|
||||
sizes.value = initialState.sizes;
|
||||
updateRoundVisibility();
|
||||
|
||||
round.addEventListener('change', updateRoundVisibility);
|
||||
|
||||
document.getElementById('pasteVariables').addEventListener('click', () => {
|
||||
vscode.postMessage({ command: 'pasteVariables' });
|
||||
});
|
||||
|
||||
document.getElementById('preview').addEventListener('click', () => {
|
||||
vscode.postMessage({ command: 'preview', text: buildSettingsText() });
|
||||
});
|
||||
|
||||
document.getElementById('generate').addEventListener('click', () => {
|
||||
vscode.postMessage({ command: 'generate', text: buildSettingsText() });
|
||||
});
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.command === 'insertVariables') {
|
||||
const next = insertTextAtSelectionValue(
|
||||
sizes.value,
|
||||
sizes.selectionStart,
|
||||
sizes.selectionEnd,
|
||||
event.data.text,
|
||||
);
|
||||
|
||||
sizes.value = next.value;
|
||||
sizes.focus();
|
||||
sizes.setSelectionRange(next.selectionStart, next.selectionEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
output.textContent = event.data.text;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
function openGenerator(context) {
|
||||
const panel = vscode.window.createWebviewPanel(
|
||||
'fluidTypographyGenerator',
|
||||
'Fluid Typography Generator',
|
||||
vscode.ViewColumn.One,
|
||||
{
|
||||
enableScripts: true,
|
||||
localResourceRoots: [context.extensionUri],
|
||||
},
|
||||
);
|
||||
|
||||
panel.webview.html = getWebviewHtml(panel.webview, getExtensionSettings());
|
||||
panel.webview.onDidReceiveMessage(async (message) => {
|
||||
if (!message || typeof message.command !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (message.command === 'pasteVariables') {
|
||||
const text = await vscode.env.clipboard.readText();
|
||||
panel.webview.postMessage({ command: 'insertVariables', text });
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof message.text !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
const css = generateCss(message.text, getExtensionSettings());
|
||||
|
||||
if (message.command === 'preview') {
|
||||
panel.webview.postMessage({ text: css });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.command === 'generate') {
|
||||
await openGeneratedCss(css);
|
||||
}
|
||||
} catch (error) {
|
||||
panel.webview.postMessage({ text: error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function activate(context) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('fluidTypography.generateFromFile', generateFromFile),
|
||||
vscode.commands.registerCommand('fluidTypography.generateFromSelection', generateFromSelection),
|
||||
vscode.commands.registerCommand('fluidTypography.openGenerator', () => openGenerator(context)),
|
||||
);
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate,
|
||||
};
|
||||
Reference in New Issue
Block a user