Skip to content

Commit

Permalink
fix: theme builder
Browse files Browse the repository at this point in the history
  • Loading branch information
jouwdan committed Mar 13, 2023
1 parent dda5d2d commit 3113332
Show file tree
Hide file tree
Showing 6 changed files with 314 additions and 102 deletions.
3 changes: 2 additions & 1 deletion apps/course/src/lib/themebuilder/Swatches.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts">
import type { SemanticNames } from './tailwind';
import { swatchColorClasses } from './settings';
/** Pass the color key name. */
export let color = 'primary';
export let color: SemanticNames = 'primary';
</script>

<div class="grid grid-cols-11 gap-0">
Expand Down
219 changes: 141 additions & 78 deletions apps/course/src/lib/themebuilder/ThemeBuilder.svelte
Original file line number Diff line number Diff line change
@@ -1,39 +1,43 @@
<script lang="ts">
import type { Writable } from 'svelte/store';
import { storePreview } from "tutors-reader-lib/src/stores/stores";
// Components
import { ProgressBar, SlideToggle, LightSwitch, CodeBlock } from '@skeletonlabs/skeleton';
// Preview Components
import { ProgressBar, CodeBlock, LightSwitch, localStorageStore, popup, SlideToggle } from '@skeletonlabs/skeleton';
import Swatch from './Swatches.svelte';
// Utilities
import { localStorageStore } from '@skeletonlabs/skeleton';
// Local Utils
import type { ColorSettings, FormTheme } from './types';
import { storePreview } from 'tutors-reader-lib/src/stores/stores';
import type { ColorSettings, FormTheme, ContrastReport } from './types';
import { inputSettings, fontSettings } from './settings';
import { type Palette, generatePalette, generateA11yOnColor } from './colors';
// Stores
const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenForm', {
import { type Palette, generatePalette, generateA11yOnColor, hexValueIsValid, getPassReport } from './colors';
import type { PopupSettings } from '@skeletonlabs/skeleton';
// Stores
/* @ts-ignore */
const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenForm', {
colors: [
{ key: 'primary', label: 'Primary', hex: '#37919b', rgb: '0 0 0', on: '0 0 0' },
{ key: 'secondary', label: 'Secondary', hex: '#3dae81', rgb: '0 0 0', on: '255 255 255' },
{ key: 'tertiary', label: 'Tertiary', hex: '#e2ac08', rgb: '0 0 0', on: '0 0 0' },
{ key: 'success', label: 'Success', hex: '#3dae81', rgb: '0 0 0', on: '0 0 0' },
{ key: 'warning', label: 'Warning', hex: '#e2ac08', rgb: '0 0 0', on: '0 0 0' },
{ key: 'error', label: 'Error', hex: '#de0d30', rgb: '0 0 0', on: '255 255 255' },
{ key: 'surface', label: 'Surface', hex: '#2a2e37', rgb: '0 0 0', on: '255 255 255' }
{ key: 'primary', label: 'Primary', hex: '#0FBA81', rgb: '0 0 0', on: '0 0 0' },
{ key: 'secondary', label: 'Secondary', hex: '#4F46E5', rgb: '0 0 0', on: '255 255 255' },
{ key: 'tertiary', label: 'Tertiary', hex: '#0EA5E9', rgb: '0 0 0', on: '0 0 0' },
{ key: 'success', label: 'Success', hex: '#84cc16', rgb: '0 0 0', on: '0 0 0' },
{ key: 'warning', label: 'Warning', hex: '#EAB308', rgb: '0 0 0', on: '0 0 0' },
{ key: 'error', label: 'Error', hex: '#D41976', rgb: '0 0 0', on: '255 255 255' },
{ key: 'surface', label: 'Surface', hex: '#495a8f', rgb: '0 0 0', on: '255 255 255' }
],
fontBase: 'system',
fontHeadings: 'system',
textColorLight: '0 0 0',
textColorDark: '255 255 255',
roundedBase: '8px',
roundedBase: '9999px',
roundedContainer: '8px',
borderBase: '1px'
});
// Local
let cssOutput: string = '';
let showThemeCSS: boolean = false;
let conReports: ContrastReport[] = getContrastReports();
function randomize(): void {
$storeThemGenForm.colors.forEach((_, i: number) => {
Expand Down Expand Up @@ -68,28 +72,55 @@ const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenFor
}
}
function getContrastReports(): ContrastReport[] {
return $storeThemGenForm.colors.map((value: ColorSettings) => ({
...value,
contrastReport: getPassReport(value.hex, value.on)
}));
}
const tooltipSettings: Omit<PopupSettings, 'target'> = {
event: 'hover',
placement: 'top'
};
const hexValuesAreValid = (colors: ColorSettings[]) => {
// Check all hex values for validity.
let valid = true;
colors?.forEach((color: ColorSettings) => {
valid = valid && hexValueIsValid(color.hex);
});
return valid;
};
// Reactive
$: if ($storeThemGenForm) {
$: if (hexValuesAreValid($storeThemGenForm.colors)) {
// Update contrast reports when hex values change and when they are valid.
conReports = getContrastReports();
}
$: if ($storeThemGenForm && hexValuesAreValid($storeThemGenForm.colors)) {
cssOutput = `
:root {
/* =~= Theme Properties =~= */
--theme-font-family-base: ${fontSettings[$storeThemGenForm.fontBase]};
--theme-font-family-heading: ${fontSettings[$storeThemGenForm.fontHeadings]};
--theme-font-color-base: ${$storeThemGenForm.textColorLight};
--theme-font-color-dark: ${$storeThemGenForm.textColorDark};
--theme-rounded-base: ${$storeThemGenForm.roundedBase};
--theme-rounded-container: ${$storeThemGenForm.roundedContainer};
--theme-border-base: ${$storeThemGenForm.borderBase};
/* =~= Theme On-X Colors =~= */
--on-primary: ${$storeThemGenForm.colors[0]?.on};
--on-secondary: ${$storeThemGenForm.colors[1]?.on};
--on-tertiary: ${$storeThemGenForm.colors[2]?.on};
--on-success: ${$storeThemGenForm.colors[3]?.on};
--on-warning: ${$storeThemGenForm.colors[4]?.on};
--on-error: ${$storeThemGenForm.colors[5]?.on};
--on-surface: ${$storeThemGenForm.colors[6]?.on};
/* =~= Theme Colors =~= */
${generateColorCSS()}
/* =~= Theme Properties =~= */
--theme-font-family-base: ${fontSettings[$storeThemGenForm.fontBase]};
--theme-font-family-heading: ${fontSettings[$storeThemGenForm.fontHeadings]};
--theme-font-color-base: ${$storeThemGenForm.textColorLight};
--theme-font-color-dark: ${$storeThemGenForm.textColorDark};
--theme-rounded-base: ${$storeThemGenForm.roundedBase};
--theme-rounded-container: ${$storeThemGenForm.roundedContainer};
--theme-border-base: ${$storeThemGenForm.borderBase};
/* =~= Theme On-X Colors =~= */
--on-primary: ${$storeThemGenForm.colors[0]?.on};
--on-secondary: ${$storeThemGenForm.colors[1]?.on};
--on-tertiary: ${$storeThemGenForm.colors[2]?.on};
--on-success: ${$storeThemGenForm.colors[3]?.on};
--on-warning: ${$storeThemGenForm.colors[4]?.on};
--on-error: ${$storeThemGenForm.colors[5]?.on};
--on-surface: ${$storeThemGenForm.colors[6]?.on};
/* =~= Theme Colors =~= */
${generateColorCSS()}
}`;
}
Expand All @@ -100,38 +131,70 @@ const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenFor
<svelte:head>{@html livePreviewStylesheet}</svelte:head>

<div class="docs-themer space-y-4">
<div class="card variant-glass-surface p-4 flex justify-between items-center">
<span>Live Preview Mode</span>
<SlideToggle size="lg" bind:checked={$storePreview} on:change={onPreviewToggle} />
<div class="card variant-glass p-4 flex justify-between items-center">
<h2>Enable Preview</h2>
<SlideToggle name="preview" bind:checked={$storePreview} on:change={onPreviewToggle} />
</div>
<div class="grid grid-cols-2 gap-4">
<!-- Theme Color -->
<section class="card col-span-2 ">
<!-- General Settings -->
<header class="p-4 col-span-2 flex justify-between items-center">
<div class="flex items-center space-x-4">
<h3>Colors</h3>
<LightSwitch />
</div>
<button class="btn variant-ghost-surface" on:click={randomize} disabled={!$storePreview}>Randomize Colors</button>
</header>
<hr />
<div class="p-4 grid grid-cols-1 gap-4">
{#each $storeThemGenForm.colors as colorRow}
<div class="grid grid-cols-1 lg:grid-cols-[170px_1fr_160px] gap-2 lg:gap-4">
<label class="input-label">
{#each $storeThemGenForm.colors as colorRow, i}
<div class="grid grid-cols-1 lg:grid-cols-[170px_1fr_200px] gap-2 lg:gap-4">
<label class="label">
<span>{colorRow.label}</span>
<div class="grid grid-cols-[auto_1fr] gap-4 place-items-end">
<input type="color" bind:value={colorRow.hex} disabled={!$storePreview} />
<input type="text" bind:value={colorRow.hex} placeholder="#BADA55" disabled={!$storePreview} />
<input class="input" type="color" bind:value={colorRow.hex} disabled={!$storePreview} />
<input class="input" type="text" bind:value={colorRow.hex} placeholder="#BADA55" disabled={!$storePreview} />
</div>
</label>
<Swatch color={colorRow.key} />
<label class="input-label">
<label>
<span>Text & Fill Color</span>
<select bind:value={colorRow.on} disabled={!$storePreview}>
{#each inputSettings.colorProps as c}<option value={c.value}>{c.label}</option>{/each}
</select>
<div class="flex">
<!-- Trigger -->
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
<!-- Select -->
<select bind:value={colorRow.on} disabled={!$storePreview}>
{#each inputSettings.colorProps as c}<option value={c.value}>{c.label}</option>{/each}
</select>
<!-- Badge -->
{#if $storePreview}
<div
class="input-group-shim !px-3"
use:popup={{ ...tooltipSettings, ...{ target: 'popup-' + i } }}
class:!text-stone-900={conReports[i].contrastReport.fails}
class:!bg-red-500={conReports[i].contrastReport.fails}
class:!text-zinc-900={conReports[i].contrastReport.largeAA}
class:!bg-amber-500={conReports[i].contrastReport.largeAA}
class:!text-slate-900={conReports[i].contrastReport.smallAAA || conReports[i].contrastReport.smallAA}
class:!bg-green-500={conReports[i].contrastReport.smallAAA || conReports[i].contrastReport.smallAA}
>
{@html conReports[i].contrastReport.report.emoji}
</div>
{/if}
</div>
<!-- Popup -->
<div
data-popup={'popup-' + i}
class="text-xs card variant-filled p-2 whitespace-nowrap"
class:!variant-filled-red-500={conReports[i].contrastReport.fails}
class:!variant-filled-amber-500={conReports[i].contrastReport.largeAA}
class:!variant-filled-green-500={conReports[i].contrastReport.smallAAA || conReports[i].contrastReport.smallAA}
>
{conReports[i].contrastReport.report.note}
<!-- Arrow -->
<div class="arrow variant-filled" />
</div>
</div>
</label>
</div>
{/each}
Expand All @@ -141,61 +204,61 @@ const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenFor
<!-- Theme Settings -->
<section class="card p-4 grid grid-cols-2 gap-4 col-span-2 lg:col-span-1">
<!-- Fonts -->
<h3 class="col-span-2">Fonts</h3>
<label class="input-label">
<h3 class="col-span-2" data-toc-ignore>Fonts</h3>
<label class="label">
<span>Base</span>
<select bind:value={$storeThemGenForm.fontBase} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.fontBase} disabled={!$storePreview}>
{#each inputSettings.fonts as f}<option value={f}>{f}</option>{/each}
</select>
</label>
<label class="input-label">
<label class="label">
<span>Headings</span>
<select bind:value={$storeThemGenForm.fontHeadings} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.fontHeadings} disabled={!$storePreview}>
{#each inputSettings.fonts as f}<option value={f}>{f}</option>{/each}
</select>
</label>
<!-- Text Color -->
<h3 class="col-span-2">Text Color</h3>
<label class="input-label">
<h3 class="col-span-2" data-toc-ignore>Text Color</h3>
<label class="label">
<span>Light Mode</span>
<select bind:value={$storeThemGenForm.textColorLight} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.textColorLight} disabled={!$storePreview}>
{#each inputSettings.colorProps as c}<option value={c.value}>{c.label}</option>{/each}
</select>
</label>
<label class="input-label">
<label class="label">
<span>Dark Mode</span>
<select bind:value={$storeThemGenForm.textColorDark} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.textColorDark} disabled={!$storePreview}>
{#each inputSettings.colorProps as c}<option value={c.value}>{c.label}</option>{/each}
</select>
</label>
<!-- Border Radius -->
<h3 class="col-span-2">Border Radius</h3>
<label class="input-label">
<h3 class="col-span-2" data-toc-ignore>Border Radius</h3>
<label class="label">
<span>Base</span>
<select bind:value={$storeThemGenForm.roundedBase} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.roundedBase} disabled={!$storePreview}>
{#each inputSettings.rounded as r}<option value={r}>{r}</option>{/each}
<option value="9999px">9999px</option>
</select>
</label>
<label class="input-label">
<label class="label">
<span>Container</span>
<select bind:value={$storeThemGenForm.roundedContainer} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.roundedContainer} disabled={!$storePreview}>
{#each inputSettings.rounded as r}<option value={r}>{r}</option>{/each}
</select>
</label>
<!-- Border Size -->
<h3 class="col-span-2">Border Size</h3>
<label class="input-label">
<h3 class="col-span-2" data-toc-ignore>Border Size</h3>
<label class="label">
<span>Base</span>
<select bind:value={$storeThemGenForm.borderBase} disabled={!$storePreview}>
<select class="select" bind:value={$storeThemGenForm.borderBase} disabled={!$storePreview}>
{#each inputSettings.border as b}<option value={b}>{b}</option>{/each}
</select>
</label>
</section>

<!-- Previews -->
<section class="card !bg-transparent p-4 space-y-8 col-span-2 lg:col-span-1">
<h3>Preview</h3>
<section class="card variant-glass p-4 space-y-8 col-span-2 lg:col-span-1">
<h3 data-toc-ignore>Preview</h3>
<!-- Buttons -->
<div class="grid grid-cols-3 gap-4">
<button class="btn variant-filled-primary">primary</button>
Expand All @@ -208,9 +271,9 @@ const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenFor
<hr class="opacity-50" />
<!-- Progress Bars -->
<div class="grid grid-cols-1 gap-4">
<ProgressBar meter="bg-primary-500" value={66} max={100} />
<ProgressBar meter="bg-secondary-500" value={50} max={100} />
<ProgressBar meter="bg-tertiary-500" value={33} max={100} />
<ProgressBar meter="bg-primary-500" track="bg-primary-500/20" value={66} max={100} />
<ProgressBar meter="bg-secondary-500" track="bg-secondary-500/20" value={50} max={100} />
<ProgressBar meter="bg-tertiary-500" track="bg-tertiary-500/20" value={33} max={100} />
</div>
<hr class="opacity-50" />
<!-- Badges -->
Expand All @@ -227,22 +290,22 @@ const storeThemGenForm: Writable<FormTheme> = localStorageStore('storeThemGenFor
<hr class="opacity-50" />
<!-- Slide Toggles -->
<div class="grid grid-cols-4 gap-4 place-items-center">
<SlideToggle accent="bg-surface-500" checked />
<SlideToggle accent="bg-primary-500" checked />
<SlideToggle checked />
<SlideToggle accent="bg-tertiary-500" checked />
<SlideToggle name="exampeSliderThree" checked />
<SlideToggle name="exampeSliderOne" active="bg-primary-500" checked />
<SlideToggle name="exampeSliderTwo" active="bg-secondary-500" checked />
<SlideToggle name="exampeSliderFour" active="bg-tertiary-500" checked />
</div>
</section>

<!-- CSS Output -->
<footer class="col-span-2 space-y-4">
{#if showThemeCSS}<CodeBlock language="css" code={cssOutput} />{/if}
<div class="card variant-glass-surface p-4 text-center">
<div class="card variant-glass p-4 text-center">
<!-- prettier-ignore -->
<button class="btn btn-lg variant-filled-primary font-bold" on:click={() => { showThemeCSS = !showThemeCSS; }} disabled={!$storePreview}>
{!showThemeCSS ? 'Show' : 'Hide'} Theme CSS
</button>
</div>
</footer>
</div>
</div>
</div>
Loading

0 comments on commit 3113332

Please sign in to comment.