fa3df3e3f4
- 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.
391 lines
11 KiB
JavaScript
391 lines
11 KiB
JavaScript
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,
|
|
};
|