fix save/load bugs
This commit is contained in:
parent
1ee86af94d
commit
3b0e5b7ada
|
|
@ -104,7 +104,48 @@ function createPlaceholderPane(paneName) {
|
||||||
* @returns {Object} Layout definition object
|
* @returns {Object} Layout definition object
|
||||||
*/
|
*/
|
||||||
export function serializeLayout(rootElement) {
|
export function serializeLayout(rootElement) {
|
||||||
const layoutNode = serializeLayoutNode(rootElement.firstChild);
|
if (!rootElement.firstChild) {
|
||||||
|
throw new Error("No layout to serialize");
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[serializeLayout] rootElement has', rootElement.children.length, 'children:');
|
||||||
|
for (let i = 0; i < rootElement.children.length; i++) {
|
||||||
|
console.log(` [${i}]:`, rootElement.children[i].className);
|
||||||
|
}
|
||||||
|
|
||||||
|
let layoutNode;
|
||||||
|
|
||||||
|
// Check if rootElement itself acts as a grid (has 2 children from a split)
|
||||||
|
if (rootElement.children.length === 2) {
|
||||||
|
// rootElement is acting as a grid container
|
||||||
|
// Check if it has grid attributes
|
||||||
|
const isHorizontal = rootElement.classList.contains("horizontal-grid");
|
||||||
|
const isVertical = rootElement.classList.contains("vertical-grid");
|
||||||
|
const percent = parseFloat(rootElement.getAttribute("lb-percent")) || 50;
|
||||||
|
|
||||||
|
if (isHorizontal || isVertical) {
|
||||||
|
console.log('[serializeLayout] rootElement is a grid, serializing both children');
|
||||||
|
layoutNode = {
|
||||||
|
type: isHorizontal ? "horizontal-grid" : "vertical-grid",
|
||||||
|
percent: percent,
|
||||||
|
children: [
|
||||||
|
serializeLayoutNode(rootElement.children[0]),
|
||||||
|
serializeLayoutNode(rootElement.children[1])
|
||||||
|
]
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// No grid classes, but has 2 children - this shouldn't happen but handle it
|
||||||
|
console.warn('[serializeLayout] rootElement has 2 children but no grid classes, serializing first child only');
|
||||||
|
layoutNode = serializeLayoutNode(rootElement.firstChild);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single child, serialize it directly
|
||||||
|
console.log('[serializeLayout] Starting from element:', rootElement.firstChild.className);
|
||||||
|
layoutNode = serializeLayoutNode(rootElement.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[serializeLayout] Serialized layout:', JSON.stringify(layoutNode, null, 2));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "Custom Layout",
|
name: "Custom Layout",
|
||||||
description: "User-created layout",
|
description: "User-created layout",
|
||||||
|
|
@ -116,26 +157,39 @@ export function serializeLayout(rootElement) {
|
||||||
* Recursively serializes a layout node
|
* Recursively serializes a layout node
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
function serializeLayoutNode(element) {
|
function serializeLayoutNode(element, depth = 0) {
|
||||||
|
const indent = ' '.repeat(depth);
|
||||||
|
console.log(`${indent}[serializeLayoutNode depth=${depth}] element:`, element.className, 'children:', element.children.length);
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
throw new Error("Cannot serialize null element");
|
throw new Error("Cannot serialize null element");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a pane
|
// Check if this is a pane (has data-pane-name attribute)
|
||||||
if (element.classList.contains("pane") && !element.classList.contains("horizontal-grid") && !element.classList.contains("vertical-grid")) {
|
// This check must come first, as panes may also have grid classes for internal layout
|
||||||
// Extract pane name from the element (stored in data attribute or class)
|
if (element.hasAttribute("data-pane-name")) {
|
||||||
const paneName = element.getAttribute("data-pane-name") || "stage";
|
// The data-pane-name is kebab-case, but we need to save the camelCase key
|
||||||
|
// that matches the panes object keys, not the name property
|
||||||
|
const dataName = element.getAttribute("data-pane-name");
|
||||||
|
|
||||||
|
// Convert kebab-case to camelCase (e.g., "timeline-v2" -> "timelineV2")
|
||||||
|
const camelCaseName = dataName.replace(/-([a-z0-9])/g, (g) => g[1].toUpperCase());
|
||||||
|
|
||||||
|
console.log(`${indent} -> Found pane: ${camelCaseName}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "pane",
|
type: "pane",
|
||||||
name: paneName
|
name: camelCaseName
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a grid
|
// Check if this is a grid (split pane structure)
|
||||||
if (element.classList.contains("horizontal-grid") || element.classList.contains("vertical-grid")) {
|
if (element.classList.contains("horizontal-grid") || element.classList.contains("vertical-grid")) {
|
||||||
const isHorizontal = element.classList.contains("horizontal-grid");
|
const isHorizontal = element.classList.contains("horizontal-grid");
|
||||||
const percent = parseFloat(element.getAttribute("lb-percent")) || 50;
|
const percent = parseFloat(element.getAttribute("lb-percent")) || 50;
|
||||||
|
|
||||||
|
console.log(`${indent} -> Found ${isHorizontal ? 'horizontal' : 'vertical'} grid with ${percent}% split`);
|
||||||
|
|
||||||
if (element.children.length !== 2) {
|
if (element.children.length !== 2) {
|
||||||
throw new Error("Grid must have exactly 2 children");
|
throw new Error("Grid must have exactly 2 children");
|
||||||
}
|
}
|
||||||
|
|
@ -144,15 +198,24 @@ function serializeLayoutNode(element) {
|
||||||
type: isHorizontal ? "horizontal-grid" : "vertical-grid",
|
type: isHorizontal ? "horizontal-grid" : "vertical-grid",
|
||||||
percent: percent,
|
percent: percent,
|
||||||
children: [
|
children: [
|
||||||
serializeLayoutNode(element.children[0]),
|
serializeLayoutNode(element.children[0], depth + 1),
|
||||||
serializeLayoutNode(element.children[1])
|
serializeLayoutNode(element.children[1], depth + 1)
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a panecontainer wrapper - recurse into it
|
||||||
|
if (element.classList.contains("panecontainer")) {
|
||||||
|
console.log(`${indent} -> Found panecontainer, recursing into child`);
|
||||||
|
if (element.children.length === 1) {
|
||||||
|
return serializeLayoutNode(element.children[0], depth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If element has only one child, recurse into it
|
// If element has only one child, recurse into it
|
||||||
if (element.children.length === 1) {
|
if (element.children.length === 1) {
|
||||||
return serializeLayoutNode(element.children[0]);
|
console.log(`${indent} -> Element has 1 child, recursing`);
|
||||||
|
return serializeLayoutNode(element.children[0], depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Cannot serialize element: ${element.className}`);
|
throw new Error(`Cannot serialize element: ${element.className}`);
|
||||||
|
|
|
||||||
150
src/main.js
150
src/main.js
|
|
@ -112,7 +112,7 @@ import { actions, initializeActions, updateAutomationName } from "./actions/inde
|
||||||
|
|
||||||
// Layout system
|
// Layout system
|
||||||
import { defaultLayouts, getLayout, getLayoutNames } from "./layouts.js";
|
import { defaultLayouts, getLayout, getLayoutNames } from "./layouts.js";
|
||||||
import { buildLayout, loadLayoutByKeyOrName, saveCustomLayout } from "./layoutmanager.js";
|
import { buildLayout, loadLayoutByKeyOrName, saveCustomLayout, serializeLayout } from "./layoutmanager.js";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
writeTextFile: writeTextFile,
|
writeTextFile: writeTextFile,
|
||||||
|
|
@ -1669,11 +1669,15 @@ async function _save(path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serialize current layout structure (panes, splits, sizes)
|
||||||
|
const serializedLayout = serializeLayout(rootPane);
|
||||||
|
|
||||||
const fileData = {
|
const fileData = {
|
||||||
version: "2.0.0",
|
version: "2.0.0",
|
||||||
width: config.fileWidth,
|
width: config.fileWidth,
|
||||||
height: config.fileHeight,
|
height: config.fileHeight,
|
||||||
fps: config.framerate,
|
fps: config.framerate,
|
||||||
|
layoutState: serializedLayout, // Save current layout structure
|
||||||
actions: undoStack,
|
actions: undoStack,
|
||||||
json: root.toJSON(),
|
json: root.toJSON(),
|
||||||
// Audio pool at the end for human readability
|
// Audio pool at the end for human readability
|
||||||
|
|
@ -2094,6 +2098,37 @@ async function _open(path, returnJson = false) {
|
||||||
context.activeObject.activeLayer = context.activeObject.layers[0];
|
context.activeObject.activeLayer = context.activeObject.layers[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore layout if saved and preference is enabled
|
||||||
|
console.log('[JS] Layout restoration check:', {
|
||||||
|
restoreLayoutFromFile: config.restoreLayoutFromFile,
|
||||||
|
hasLayoutState: !!file.layoutState,
|
||||||
|
layoutState: file.layoutState
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config.restoreLayoutFromFile && file.layoutState) {
|
||||||
|
try {
|
||||||
|
console.log('[JS] Restoring saved layout:', file.layoutState);
|
||||||
|
// Clear existing layout
|
||||||
|
while (rootPane.firstChild) {
|
||||||
|
rootPane.removeChild(rootPane.firstChild);
|
||||||
|
}
|
||||||
|
layoutElements.length = 0;
|
||||||
|
canvases.length = 0;
|
||||||
|
|
||||||
|
// Build layout from saved state
|
||||||
|
buildLayout(rootPane, file.layoutState, panes, createPane, splitPane);
|
||||||
|
|
||||||
|
// Update UI after layout change
|
||||||
|
updateAll();
|
||||||
|
updateUI();
|
||||||
|
console.log('[JS] Layout restored successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[JS] Failed to restore layout, using default:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[JS] Skipping layout restoration');
|
||||||
|
}
|
||||||
|
|
||||||
// Restore audio tracks and clips to the Rust backend
|
// Restore audio tracks and clips to the Rust backend
|
||||||
// The fromJSON method only creates JavaScript objects,
|
// The fromJSON method only creates JavaScript objects,
|
||||||
// but doesn't initialize them in the audio engine
|
// but doesn't initialize them in the audio engine
|
||||||
|
|
@ -4994,15 +5029,25 @@ async function startup() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('[startup] window.openedFiles:', window.openedFiles);
|
||||||
|
console.log('[startup] config.reopenLastSession:', config.reopenLastSession);
|
||||||
|
console.log('[startup] config.recentFiles:', config.recentFiles);
|
||||||
|
|
||||||
|
// Always update start screen data so it's ready when needed
|
||||||
|
await updateStartScreen(config);
|
||||||
|
|
||||||
if (!window.openedFiles?.length) {
|
if (!window.openedFiles?.length) {
|
||||||
if (config.reopenLastSession && config.recentFiles?.length) {
|
if (config.reopenLastSession && config.recentFiles?.length) {
|
||||||
|
console.log('[startup] Reopening last session:', config.recentFiles[0]);
|
||||||
document.body.style.cursor = "wait"
|
document.body.style.cursor = "wait"
|
||||||
setTimeout(()=>_open(config.recentFiles[0]), 10)
|
setTimeout(()=>_open(config.recentFiles[0]), 10)
|
||||||
} else {
|
} else {
|
||||||
// Show start screen instead of new file dialog
|
console.log('[startup] Showing start screen');
|
||||||
await updateStartScreen(config);
|
// Show start screen
|
||||||
showStartScreen();
|
showStartScreen();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.log('[startup] Files already opened, skipping start screen');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6441,6 +6486,11 @@ async function renderMenu() {
|
||||||
action: actions.selectNone.create,
|
action: actions.selectNone.create,
|
||||||
accelerator: getShortcut("selectNone"),
|
accelerator: getShortcut("selectNone"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: "Preferences",
|
||||||
|
enabled: true,
|
||||||
|
action: showPreferencesDialog,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -10231,6 +10281,100 @@ function showSavePresetDialog(container) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show preferences dialog
|
||||||
|
function showPreferencesDialog() {
|
||||||
|
const dialog = document.createElement('div');
|
||||||
|
dialog.className = 'modal-overlay';
|
||||||
|
dialog.innerHTML = `
|
||||||
|
<div class="modal-dialog preferences-dialog">
|
||||||
|
<h3>Preferences</h3>
|
||||||
|
<form id="preferences-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Default BPM</label>
|
||||||
|
<input type="number" id="pref-bpm" min="20" max="300" value="${config.bpm}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Default Framerate</label>
|
||||||
|
<input type="number" id="pref-framerate" min="1" max="120" value="${config.framerate}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Default File Width</label>
|
||||||
|
<input type="number" id="pref-width" min="100" max="10000" value="${config.fileWidth}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Default File Height</label>
|
||||||
|
<input type="number" id="pref-height" min="100" max="10000" value="${config.fileHeight}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Scroll Speed</label>
|
||||||
|
<input type="number" id="pref-scroll-speed" min="0.1" max="10" step="0.1" value="${config.scrollSpeed}" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="pref-reopen-session" ${config.reopenLastSession ? 'checked' : ''} />
|
||||||
|
Reopen last session on startup
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="pref-restore-layout" ${config.restoreLayoutFromFile ? 'checked' : ''} />
|
||||||
|
Restore layout when opening files
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="pref-debug" ${config.debug ? 'checked' : ''} />
|
||||||
|
Enable debug mode
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" class="btn-cancel">Cancel</button>
|
||||||
|
<button type="submit" class="btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(dialog);
|
||||||
|
|
||||||
|
// Focus first input
|
||||||
|
setTimeout(() => dialog.querySelector('#pref-bpm')?.focus(), 100);
|
||||||
|
|
||||||
|
// Handle cancel
|
||||||
|
dialog.querySelector('.btn-cancel').addEventListener('click', () => {
|
||||||
|
dialog.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle save
|
||||||
|
dialog.querySelector('#preferences-form').addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Update config values
|
||||||
|
config.bpm = parseInt(dialog.querySelector('#pref-bpm').value);
|
||||||
|
config.framerate = parseInt(dialog.querySelector('#pref-framerate').value);
|
||||||
|
config.fileWidth = parseInt(dialog.querySelector('#pref-width').value);
|
||||||
|
config.fileHeight = parseInt(dialog.querySelector('#pref-height').value);
|
||||||
|
config.scrollSpeed = parseFloat(dialog.querySelector('#pref-scroll-speed').value);
|
||||||
|
config.reopenLastSession = dialog.querySelector('#pref-reopen-session').checked;
|
||||||
|
config.restoreLayoutFromFile = dialog.querySelector('#pref-restore-layout').checked;
|
||||||
|
config.debug = dialog.querySelector('#pref-debug').checked;
|
||||||
|
|
||||||
|
// Save config to localStorage
|
||||||
|
await saveConfig();
|
||||||
|
|
||||||
|
dialog.remove();
|
||||||
|
|
||||||
|
console.log('Preferences saved:', config);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close on background click
|
||||||
|
dialog.addEventListener('click', (e) => {
|
||||||
|
if (e.target === dialog) {
|
||||||
|
dialog.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to convert MIDI note number to note name
|
// Helper function to convert MIDI note number to note name
|
||||||
function midiToNoteName(midiNote) {
|
function midiToNoteName(midiNote) {
|
||||||
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export function createStartScreen(callback) {
|
||||||
startScreenContainer = document.createElement('div');
|
startScreenContainer = document.createElement('div');
|
||||||
startScreenContainer.id = 'startScreen';
|
startScreenContainer.id = 'startScreen';
|
||||||
startScreenContainer.className = 'start-screen';
|
startScreenContainer.className = 'start-screen';
|
||||||
|
startScreenContainer.style.display = 'none'; // Hidden by default
|
||||||
|
|
||||||
// Create welcome title
|
// Create welcome title
|
||||||
const title = document.createElement('h1');
|
const title = document.createElement('h1');
|
||||||
|
|
@ -170,6 +171,8 @@ function createFocusCard(focus) {
|
||||||
export async function updateStartScreen(config) {
|
export async function updateStartScreen(config) {
|
||||||
if (!startScreenContainer) return;
|
if (!startScreenContainer) return;
|
||||||
|
|
||||||
|
console.log('[updateStartScreen] config.recentFiles:', config.recentFiles);
|
||||||
|
|
||||||
// Update last session
|
// Update last session
|
||||||
const lastSessionDiv = document.getElementById('lastSessionFile');
|
const lastSessionDiv = document.getElementById('lastSessionFile');
|
||||||
if (lastSessionDiv) {
|
if (lastSessionDiv) {
|
||||||
|
|
|
||||||
19
src/state.js
19
src/state.js
|
|
@ -125,9 +125,27 @@ export async function loadConfig() {
|
||||||
// Merge loaded config with defaults
|
// Merge loaded config with defaults
|
||||||
Object.assign(config, deepMerge({ ...config }, loaded));
|
Object.assign(config, deepMerge({ ...config }, loaded));
|
||||||
|
|
||||||
|
// Ensure recentFiles is always an array (fix legacy string format)
|
||||||
|
let needsResave = false;
|
||||||
|
if (typeof config.recentFiles === 'string') {
|
||||||
|
config.recentFiles = config.recentFiles.split(',').filter(f => f.length > 0);
|
||||||
|
needsResave = true;
|
||||||
|
} else if (!Array.isArray(config.recentFiles)) {
|
||||||
|
config.recentFiles = [];
|
||||||
|
needsResave = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Make config accessible to widgets via context
|
// Make config accessible to widgets via context
|
||||||
context.config = config;
|
context.config = config;
|
||||||
|
|
||||||
|
console.log('[loadConfig] Loaded config.recentFiles:', config.recentFiles);
|
||||||
|
|
||||||
|
// Re-save config if we had to fix the format
|
||||||
|
if (needsResave) {
|
||||||
|
console.log('[loadConfig] Re-saving config to fix array format');
|
||||||
|
await saveConfig();
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error loading config, using defaults:", error);
|
console.log("Error loading config, using defaults:", error);
|
||||||
|
|
@ -154,6 +172,7 @@ export async function addRecentFile(filePath) {
|
||||||
filePath,
|
filePath,
|
||||||
...config.recentFiles.filter(file => file !== filePath)
|
...config.recentFiles.filter(file => file !== filePath)
|
||||||
].slice(0, 10);
|
].slice(0, 10);
|
||||||
|
console.log('[addRecentFile] Added file, recentFiles now:', config.recentFiles);
|
||||||
await saveConfig();
|
await saveConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1992,7 +1992,8 @@ button {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group input,
|
.form-group input[type="text"],
|
||||||
|
.form-group input[type="number"],
|
||||||
.form-group textarea {
|
.form-group textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
|
|
@ -2005,6 +2006,19 @@ button {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label:has(input[type="checkbox"]) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group input:focus,
|
.form-group input:focus,
|
||||||
.form-group textarea:focus {
|
.form-group textarea:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -2219,7 +2233,7 @@ button {
|
||||||
font-size: 1.1em;
|
font-size: 1.1em;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--surface-light);
|
background: var(--surface-light);
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--shadow);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
@ -2267,7 +2281,7 @@ button {
|
||||||
width: 180px;
|
width: 180px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: var(--surface-light);
|
background: var(--surface-light);
|
||||||
border: 2px solid var(--border-light);
|
border: 2px solid var(--shadow);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -2290,7 +2304,7 @@ button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 3px solid var(--button-hover);
|
border: 3px solid var(--shadow);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue