Skip to content

Commit

Permalink
feat: add auto-save and restoration features to the editor
Browse files Browse the repository at this point in the history
Resolves #5 by implementing real-time auto-save functionality and session restoration from local
storage in the theme editor.

Changes to SCSS files are now saved in real-time to the browser’s local storage. Upon returning to
the theme editor, the system automatically restores the integrator's last session from local
storage.

This commit introduces the following changes:

- Added a `ConfirmationDialog` component that prompts users to confirm before resetting the editor
  settings. This ensures that changes are not lost accidentally.
- Introduced a `WorkspaceProvider` that manages the persistence and retrieval of the workspace state
  from local storage.
  • Loading branch information
jboix committed Apr 18, 2024
1 parent 30e0846 commit 2a23c87
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 26 deletions.
13 changes: 12 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,20 @@ <h2>Craft your theme</h2>
<toggle-pane-button title="Select a file to edit" id="navigation-button">
<tree-view id="navigation"></tree-view>
</toggle-pane-button>
<button id="download">
<button class="pbte-btn" id="download-button">
Export as .zip
</button>
<button class="pbte-btn alert" id="reset-button">
Reset changes
</button>
<confirmation-dialog id="reset-confirmation-dialog">
<span slot="title">Reset Your Workspace?</span>
<span slot="content">
Are you sure you want to reset your workspace? All unsaved changes will be lost and the
theme will revert to the default settings. This action cannot be undone.
</span>
<span slot="accept">Confirm Reset</span>
</confirmation-dialog>
</div>
<css-editor id="editor"></css-editor>
</section>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"prebuild": "node .prebuild.js",
"build": "vite build",
"test": "vitest run --coverage --reporter=verbose --silent",
"test": "npm run prebuild && vitest run --coverage --reporter=verbose --silent",
"eslint": "eslint {src,test}/**/*.js",
"eslint:fix": "eslint {src,test}/**/*.js --fix",
"outdated": "npm outdated",
Expand Down
18 changes: 15 additions & 3 deletions scss/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -107,20 +107,28 @@ footer {
height: 100%;
}

#download {
.pbte-btn, .pbte-alert-btn {
padding: 1em;
color: var(--button-foreground);
color: var(--button-color);
background: var(--button-background);
border: none;
border-radius: var(--button-border-radius);
cursor: pointer;
transition: background-color 0.3s ease;
}

#download:hover {
.pbte-btn:hover {
background-color: var(--button-hover-background);
}

.pbte-btn.alert {
background: #9D4040;
}

.pbte-btn.alert:hover {
background: #733030;
}

input {
margin: 0;
padding: 0.5em 0.2em;
Expand All @@ -131,6 +139,10 @@ input {
outline: none;
}

confirmation-dialog {
max-width: 360px;
}

*:not(:defined) {
display: none;
}
141 changes: 141 additions & 0 deletions src/components/confirmation-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { html, LitElement, unsafeCSS } from 'lit';
import { createRef, ref } from 'lit/directives/ref.js';
import confirmationDialogStyle from './confirmation-dialog.scss?inline';

/**
* `ConfirmationDialog` is a LitElement that renders a modal dialog window which
* can be used to confirm or reject an action. It encapsulates user interaction
* in the form of "accept" or "cancel" actions.
*
* @fires ConfirmationDialog#close Fired when the dialog is closed, with `detail.accepted` indicating if the "accept" was clicked.
* @fires ConfirmationDialog#open Fired when the dialog is opened.
*
* @part body - The container of the dialog's content.
* @part actions - The container of the dialog's action buttons.
* @part button - Parts for styling the buttons individually.
* @part button cancel - Part for styling the cancel button.
* @part button accept - Part for styling the accept button.
*
* @slot title - The dialog's title. Provides context about the dialog's purpose.
* @slot content - The main content of the dialog, such as descriptions or questions.
* @slot cancel - (Optional) Customizes the cancel button text. Defaults to "Cancel."
* @slot accept - (Optional) Customizes the accept button text. Defaults to "Accept."
*
* @cssproperty [--dialog-padding=1em] - The padding inside the dialog.
* @cssproperty [--dialog-background-color=#242424] - The background color of the dialog.
* @cssproperty [--dialog-border=1px solid #666] - The border style for the dialog.
* @cssproperty [--dialog-color=rgb(255 255 255 / 87%)] - The text color inside the dialog.
* @cssproperty [--dialog-title-font-weight=600] - The font weight for the dialog title.
* @cssproperty [--dialog-border-radius=0.5em] - The border radius of the dialog.
* @cssproperty [--dialog-max-width=360px] - The maximum width of the dialog.
* @cssproperty [--button-color=rgb(255 255 255 / 87%)] - The text color of the buttons.
* @cssproperty [--button-border-radius=0.5em] - The border radius of the buttons.
* @cssproperty [--accept-button-background=#9D4040] - The background color of the accept button.
* @cssproperty [--accept-button-hover-background=#733030] - The background color of the accept button on hover.
* @cssproperty [--cancel-button-background=rgb(255 255 255 / 0%)] - The background color of the cancel button.
* @cssproperty [--cancel-button-hover-background=rgb(255 255 255 / 20%)] - The background color of the cancel button on hover.
* @cssproperty [--cancel-button-hover-color=rgb(0 0 0 / 87%)] - The text color of the cancel button on hover.
*
* @example
* <confirmation-dialog>
* <span slot="title">Are you sure ?</span>
* <span slot="content">This action is irreversible</span>
* </confirmation-dialog>
*/
class ConfirmationDialog extends LitElement {
static styles = unsafeCSS(confirmationDialogStyle);

static properties = {
open: {
type: Boolean,
state: true
},
accepted: {
type: Boolean,
state: true
}
};

/**
* Reference to the dialog HTML element.
*
* @type {import('lit/directives/ref').Ref<HTMLDialogElement>}
* @private
*/
#dialog = createRef();

constructor() {
super();
this.open = false;
this.accepted = false;
}

render() {
return html`
<dialog ${ref(this.#dialog)}>
<div part="body">
<slot name="title"></slot>
<slot name="content"></slot>
<div part="actions">
<button part="button cancel" @click=${() => this.#close(false)}>
<slot name="cancel">Cancel</slot>
</button>
<button part="button accept" @click=${() => this.#close(true)}>
<slot name="accept">Accept</slot>
</button>
</div>
</div>
</dialog>
`;
}

#close(accepted = false) {
this.open = false;
this.accepted = accepted;
}

updated(_changedProperties) {
super.updated(_changedProperties);

if (_changedProperties.has('open')) {
this.#updateDialog();
}
}

#updateDialog() {
if (this.open) {
this.#dialog.value.showModal();
/**
* Custom event dispatched when the dialog is opened.
*
* @event ConfirmationDialog#open
* @type {CustomEvent}
*/
this.dispatchEvent(new CustomEvent('open'));
} else {
this.#dialog.value.close();
/**
* Custom event dispatched when the dialog is closed. Includes whether the closure was an acceptance.
*
* @event ConfirmationDialog#close
* @type {CustomEvent}
* @property {Object} detail - The event detail object.
* @property {boolean} detail.accepted - Indicates if the dialog was closed with an acceptance.
*/
this.dispatchEvent(new CustomEvent('close', { detail: { accepted: this.accepted } }));
}
this.accepted = false;
}

/**
* Toggles the dialog's open state.
*
* @param {boolean} [open] Specifies the desired open state. If not provided,
* it toggles the current state.
*/
toggle(open) {
this.open = open ?? !this.open;
}
}

customElements.define('confirmation-dialog', ConfirmationDialog);
79 changes: 79 additions & 0 deletions src/components/confirmation-dialog.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
:host {
--dialog-padding: 1em;
--dialog-background-color: #242424;
--dialog-border: 1px solid #666;
--dialog-color: rgb(255 255 255 / 87%);
--dialog-title-font-weight: 600;
--dialog-border-radius: 0.5em;
--dialog-max-width: 360px;
--accept-button-background: #9D4040;
--accept-button-hover-background: #733030;
--cancel-button-background: rgb(255 255 255 / 0%);
--cancel-button-hover-background: rgb(255 255 255 / 20%);
--cancel-button-hover-color: rgb(0 0 0 / 87%);
--button-color: rgb(255 255 255 / 87%);
--button-border-radius: 0.5em;
}

dialog {
max-width: var(--dialog-max-width);
padding: var(--dialog-padding);
overflow: visible;
color: var(--dialog-color);
background-color: var(--dialog-background-color);
border: var(--dialog-border);
border-radius: var(--dialog-border-radius);
}

dialog::backdrop {
background: rgb(0 0 0 / 50%);
}

::slotted([slot="title"]) {
font-weight: var(--dialog-title-font-weight);
}

::slotted([slot="content"]) {
text-align: justify;
}

[part="body"] {
display: flex;
flex-direction: column;
gap: 0.5em;
}

[part="actions"] {
display: flex;
gap: 0.5em;
justify-content: right;
}

[part~="button"] {
padding: 1em;
color: var(--button-color);
background-color: transparent;
border: none;
border-radius: var(--button-border-radius);
cursor: pointer;
transition-timing-function: ease;
transition-duration: 0.3s;
transition-property: color, background-color;
}

[part="button cancel"] {
background: var(--cancel-button-background);
}

[part="button cancel"]:hover {
color: var(--cancel-button-hover-color);
background-color: var(--cancel-button-hover-background);
}

[part="button accept"] {
background: var(--accept-button-background);
}

[part="button accept"]:hover {
background-color: var(--accept-button-hover-background);
}
2 changes: 1 addition & 1 deletion src/components/toggle-pane-button.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
[part="button"] {
position: relative; /* Needed for the chevron */
padding: 1em;
color: var(--button-foreground);
color: var(--button-color);
background: var(--button-background);
border: none;
border-radius: var(--button-border-radius);
Expand Down
60 changes: 42 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,66 @@
import './components/css-editor.js';
import './components/resizable-split-view.js';
import './components/preview-box.js';
import './components/tree-view.js';
import './components/toggle-pane-button.js';
import './components/confirmation-dialog.js';
import './components/preview-box.js';
import './components/css-editor.js';
import sassCompiler from './workspace/workspace.js';
import WorkspaceProvider from './workspace/workspace-provider.js';

// Preview Initialisation
const preview = document.getElementById('preview');
const sourceInput = document.getElementById('src-input');

sourceInput.addEventListener('keyup', (event) => {
const src = event.target.value;

if (event.key === 'Enter' && src) {
preview.mediaSrc = src;
}
});

preview.appliedCss = sassCompiler.compile();

// Editor Initialisation
const editor = document.getElementById('editor');
let currentItem = sassCompiler.mainScss;

editor.setValue(sassCompiler.mainScss.content);
editor.addEventListener('value-changed', (event) => {
currentItem.content = event.detail.value;
preview.appliedCss = sassCompiler.compile();
WorkspaceProvider.saveWorkspace(sassCompiler.workspace);
});

// Navigation Control
const navigation = document.getElementById('navigation');
const navigationButton = document.getElementById('navigation-button');
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const downloadButton = document.getElementById('download');
const sourceInput = document.getElementById('src-input');

navigation.items = sassCompiler.workspace;
navigationButton.label = currentItem.name;
editor.setValue(currentItem.content);

navigation.addEventListener('selected', (event) => {
currentItem = event.detail;
navigationButton.label = currentItem.name;
editor.setValue(currentItem.content);
navigationButton.opened = false;
});

preview.appliedCss = sassCompiler.compile();
editor.addEventListener('value-changed', (event) => {
currentItem.content = event.detail.value;
preview.appliedCss = sassCompiler.compile();
});
navigationButton.label = sassCompiler.mainScss.name;

// Download Control
const downloadButton = document.getElementById('download-button');

downloadButton.addEventListener('click', () => sassCompiler.download());

sourceInput.addEventListener('keyup', (event) => {
const src = event.target.value;
// Reset Control
const resetButton = document.getElementById('reset-button');
const confirmationDialog = document.getElementById('reset-confirmation-dialog');

if (event.key === 'Enter' && src) {
preview.mediaSrc = src;
resetButton.addEventListener('click', () => {
confirmationDialog.toggle();
});
confirmationDialog.addEventListener('close', (event) => {
if (event.detail.accepted) {
WorkspaceProvider.clear();
window.location.reload();
}
});
Loading

0 comments on commit 2a23c87

Please sign in to comment.