Skip to content

Commit

Permalink
feat: complete palette page
Browse files Browse the repository at this point in the history
  • Loading branch information
MM25Zamanian committed Jun 5, 2024
1 parent 93b15c2 commit d3cb013
Show file tree
Hide file tree
Showing 17 changed files with 210 additions and 72 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,52 @@
# Hybrid UI Theme Builder

Welcome to the Hybrid UI Theme Builder! This powerful tool empowers developers to create custom themes for their Hybrid UI applications with ease and flexibility.

## Description

Hybrid UI Theme Builder is a dedicated theme builder designed specifically for Hybrid UI. It provides developers with an intuitive and user-friendly interface to create, customize, and manage themes for their applications. With this tool, you can give your Hybrid UI applications a unique look and feel that aligns with your brand or personal preference.

## Features

- **Customization**: Tailor every aspect of your Hybrid UI application's theme, from colors and typography to layout and spacing.
- **Ease of Use**: The Hybrid UI Theme Builder is designed with simplicity in mind, making it easy for developers of all skill levels to use.
- **Theme Management**: Create, save, and manage multiple themes for different applications or use cases.
- **Real-time Preview**: See your changes in real-time as you customize your theme, ensuring you get the perfect look.
- **Export Themes**: Once you're satisfied with your theme, you can easily export it for use in your Hybrid UI applications.

## Keywords

- Hybrid-UI
- Theme
- Builder
- Customization
- Theming
- UI-Components

## Installation

To get started with the Hybrid UI Theme Builder, follow the installation instructions provided in the [Installation Guide](link-to-installation-guide).

## Usage

For detailed instructions on how to use the Hybrid UI Theme Builder, please refer to the [Usage Guide](link-to-usage-guide).

## Contributing

We welcome contributions from the community. If you wish to contribute, please take a moment to review our [Contributing Guidelines](link-to-contributing-guidelines).

## License

The Hybrid UI Theme Builder is open-source software licensed under the [MIT License](LICENSE).

## Support

If you encounter any problems or have any questions, please open an issue on our [Issues](link-to-issues) page.

## Acknowledgements

We would like to thank the Hybrid UI community for their support and contributions to this project.

Happy theming with the Hybrid UI Theme Builder!

[Icon Kitchen](https://icon.kitchen/i/H4sIAAAAAAAAAx2PQWvDMAyF%2F0rQrhmsrIOR21IYgx2X2yhFiRXXRLFTOW4Jpf%2B9sk9Cn957ku5wRU4UobmDQZm6M80EzYgcqYbRHtgtKGseR9ICEpI3UIMbgtd2CBzkxOQjPGro7d8ZF%2FVDvCQnA5Mqe3vIIoUv5nO%2F270rW2JrM9gPOH68FfAdfFkz4ux40%2BHXhBtWv%2BjR4IQ5fomdWzWzgZ%2BuLaYOLTtfyNaLM1W5v2qTY0OSPXMwifN%2F%2F3CjXk3ojQRn1msOYNxe4xqE4Ph4Ah%2BcQuMLAQAA)
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
<script type="module" src="/src/main.ts"></script>
</head>

<body class="bg-surface max-w-screen-sm mx-auto overflow-x-hidden overflow-y-auto w-full h-full text-bodyMedium text-onSurface"></body>
<body class="bg-surface max-w-screen-lg mx-auto overflow-x-hidden overflow-y-auto w-full h-full text-bodyMedium text-onSurface"></body>
</html>
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "hybrid-theme-builder",
"description": "A theme builder for Hybrid UI, allowing developers to create custom themes for their applications.",
"type": "module",
"prettier": "@alwatr/prettier-config",
"eslintConfig": {
Expand Down Expand Up @@ -44,6 +45,7 @@
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
"autoprefixer": "^10.4.19",
"chroma.ts": "^1.0.10",
"cssnano": "^7.0.1",
"cssnano-preset-advanced": "^7.0.1",
"eslint": "8.57.0",
Expand Down
49 changes: 2 additions & 47 deletions src/signal/color.context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ContextSignal} from '@gecut/signal';
import debounce from '@gecut/utilities/debounce.js';
import json from '@gecut/utilities/local-storage-json.js';
import {hexFromArgb, argbFromRgb} from '@material/material-color-utilities';
import * as chroma from 'chroma.ts';

import {themeContext} from './theme.context.js';

Expand All @@ -21,10 +21,7 @@ colorContext.setValue({

colorContext.subscribe(
debounce((color: HSLColor) => {
const rgb = hslToRgb(Number(color.hue), Number(color.saturation), Number(color.lightness));
const argb = argbFromRgb(rgb.red, rgb.green, rgb.blue);
const hex = hexFromArgb(argb);

const hex = chroma.hsl(Number(color.hue), Number(color.saturation) / 100, Number(color.lightness) / 100).hex('rgb');
themeContext.setValue(hex);

json.set('HTB.HUE', color.hue);
Expand All @@ -36,45 +33,3 @@ colorContext.subscribe(
receivePrevious: true,
},
);

function hslToRgb(hue: number, saturation: number, lightness: number) {
hue = hue % 360; // Ensure hue is in the range of 0 to 360
const chroma = saturation * Math.min(lightness, 1 - lightness);
const huePrime = hue / 60;
const x = chroma * (1 - Math.abs((huePrime % 2) - 1));
let red = 0,
green = 0,
blue = 0;

if (huePrime >= 0 && huePrime < 1) {
red = chroma;
green = x;
}
else if (huePrime >= 1 && huePrime < 2) {
red = x;
green = chroma;
}
else if (huePrime >= 2 && huePrime < 3) {
green = chroma;
blue = x;
}
else if (huePrime >= 3 && huePrime < 4) {
green = x;
blue = chroma;
}
else if (huePrime >= 4 && huePrime < 5) {
red = x;
blue = chroma;
}
else if (huePrime >= 5 && huePrime < 6) {
red = chroma;
blue = x;
}

const m = lightness - 0.5 * chroma;
red = Math.round((red + m) * 255);
green = Math.round((green + m) * 255);
blue = Math.round((blue + m) * 255);

return {red, green, blue};
}
2 changes: 1 addition & 1 deletion src/ui/app-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ document.documentElement.classList.add('color-scheme-light');
render(
html`
${header()}
<main role="main" class="w-full py-20 px-4">${gecutContext(routerContext, (page) => page)}</main>
<main role="main" class="w-full pt-20 pb-32 px-4">${gecutContext(routerContext, (page) => page)}</main>
${footer()}
`,
document.body,
Expand Down
1 change: 1 addition & 0 deletions src/ui/assets/svg/dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/ui/assets/svg/download.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/ui/assets/svg/light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 6 additions & 5 deletions src/ui/components/footer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {gecutContext} from '@gecut/lit-helper/directives/context.js';
import debounce from '@gecut/utilities/debounce.js';
import {hexFromArgb} from '@material/material-color-utilities';
import {styleMap} from 'lit/directives/style-map.js';
import {html} from 'lit/html.js';

import {colorContext} from '../../signal/color.context.js';
import {themeContext} from '../../signal/theme.context.js';
import {paletteContext} from '../../signal/palette.context.js';

const colorPickerChange = debounce((event: InputEvent) => {
const target = event.target as HTMLInputElement;
Expand All @@ -25,7 +26,7 @@ export function footer() {

return html`
<footer class="fixed bottom-0 inset-x-0 bg-surfaceContainer translucent flex flex-col p-4">
<div class="max-w-screen-sm w-full mx-auto flex justify-center items-center gap-4">
<div class="max-w-screen-lg w-full mx-auto flex justify-center items-center gap-4">
<div class="flex flex-col gap-4">
<span>Hue:</span>
<span>Saturation:</span>
Expand Down Expand Up @@ -80,12 +81,12 @@ export function footer() {
`,
)}
${gecutContext(
themeContext,
(theme) => html`
paletteContext,
(palette) => html`
<div
class="p-2 w-20 text-center rounded-full shadow-md"
style=${styleMap({
backgroundColor: `${theme}`,
backgroundColor: `${hexFromArgb(palette.source)}`,
})}
>
Material
Expand Down
63 changes: 62 additions & 1 deletion src/ui/components/header.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import {gecutIconButton} from '@gecut/components';
import {gecutContext} from '@gecut/lit-helper/directives/context.js';
import clipboard from '@gecut/utilities/clipboard.js';
import debounce from '@gecut/utilities/debounce.js';
import {nextIdleCallback} from '@gecut/utilities/wait/polyfill.js';
import {Hct, argbFromHex} from '@material/material-color-utilities';
import {html} from 'lit/html.js';

import {colorContext} from '../../signal/color.context.js';
import {paletteContext} from '../../signal/palette.context.js';
import {themeContext} from '../../signal/theme.context.js';
import {titleContext} from '../../signal/title.context.js';
import darkIcon from '../assets/svg/dark.svg?raw';
import downloadIcon from '../assets/svg/download.svg?raw';
import lightIcon from '../assets/svg/light.svg?raw';
import shareIcon from '../assets/svg/share.svg?raw';

export function header() {
return html`
<header class="fixed top-0 inset-x-0 bg-surfaceContainer translucent flex flex-col">
<div class="max-w-screen-sm mx-auto flex justify-between py-4 gap-4 w-full h-full relative px-4">
<div class="max-w-screen-lg mx-auto flex justify-between py-4 gap-4 w-full h-full relative px-4">
<div class="flex flex-1 justify-start items-center">
<div class="size-10 rounded-full overflow-hidden m-1">
<img src="/icon-192-maskable.png" class="rounded-full" />
Expand All @@ -22,6 +30,30 @@ export function header() {
</div>
<div class="flex flex-1 justify-end items-center">
${gecutIconButton({
toggle: true,
svg: lightIcon,
selectedIcon: {
svg: darkIcon,
},
events: {
change: debounce((event: Event) => {
const target = event.target as HTMLInputElement;
const checked = target.checked;
if (checked) {
document.documentElement.classList.remove('color-scheme-light');
document.documentElement.classList.add('color-scheme-dark');
}
else {
document.documentElement.classList.remove('color-scheme-dark');
document.documentElement.classList.add('color-scheme-light');
}
paletteContext.renotify();
}, 1000 / 30),
},
})}
${gecutIconButton({
svg: shareIcon,
events: {
Expand All @@ -47,6 +79,35 @@ export function header() {
},
},
})}
${gecutIconButton({
svg: downloadIcon,
events: {
click: (event: Event) => {
const target = event.target as HTMLElement;
const download = () =>
new Promise<void>((resolve) => {
const fileName = 'h' + Math.ceil(Hct.fromInt(argbFromHex(themeContext.getValue() ?? '#000')).hue) + '.css';
const a = document.createElement('a'); // Create "a" element
const blob = new Blob([':root{', document.documentElement.getAttribute('style') ?? '', '}'], {
type: 'text/css',
}); // Create a blob (file-like object)
const url = URL.createObjectURL(blob); // Create an object URL from blob
a.setAttribute('href', url); // Set "a" element link
a.setAttribute('download', fileName); // Set download filename
setTimeout(() => nextIdleCallback(() => resolve(a.click())), 1024);
});
target.setAttribute('loading', '');
download().finally(() => {
target.removeAttribute('loading');
});
},
},
})}
</div>
</div>
</header>
Expand Down
39 changes: 39 additions & 0 deletions src/ui/components/palette-reference-colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {mapObject} from '@gecut/lit-helper/utilities/map-object.js';
import {html} from 'lit/html.js';

const themeScheme: Record<'primary' | 'secondary' | 'tertiary' | 'neutral' | 'error' | 'neutral-variant', number[]> = {
primary: [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100],
secondary: [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100],
tertiary: [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100],
neutral: [0, 4, 6, 10, 12, 17, 20, 22, 25, 30, 35, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100],
'neutral-variant': [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100],
error: [0, 10, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100],
};

export function paletteReferenceColors() {
return html`
<div class="flex flex-col gap-2 px-2">
${mapObject(
null,
themeScheme,
(tones, key) => html`
<h2 class="text-titleMedium text-onSurfaceVariant capitalize mt-4">${key}</h2>
<div class="flex rounded-md overflow-hidden shadow">
${tones.map(
(tone, index) => html`
<div
class="h-12 grow text-bodySmall flex items-center justify-center"
style="background-color:rgb(var(--ref-palette-${key + tone}));"
>
<span class="hidden md:inline" style="color:rgb(var(--ref-palette-${key + tones[tones.length - 1 - index]}));">
${tone}
</span>
</div>
`,
)}
</div>
`,
)}
</div>
`;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {map} from '@gecut/lit-helper/utilities/map.js';
import clipboard from '@gecut/utilities/clipboard.js';
import {argbFromHex, hexFromArgb, themeFromSourceColor} from '@material/material-color-utilities';
import {hexFromArgb, type Theme} from '@material/material-color-utilities';
import {html} from 'lit/html.js';

const capitalize = (str: string) => {
Expand Down Expand Up @@ -38,9 +38,10 @@ const colors = {
'on surface variant': 'bg-onSurfaceVariant text-surfaceVariant',
} as const;

export function palettePage(sourceColor: string, mode: 'light' | 'dark') {
const theme = themeFromSourceColor(argbFromHex(sourceColor));

export function paletteSystemColors(
theme: Theme,
mode: 'light' | 'dark' = document.documentElement.classList.contains('color-scheme-dark') ? 'dark' : 'light',
) {
return html`
<div class="flex flex-wrap">
${map(null, Object.keys(colors) as (keyof typeof colors)[], (color: keyof typeof colors) => {
Expand Down
9 changes: 0 additions & 9 deletions src/ui/pages/builder.page.ts

This file was deleted.

18 changes: 18 additions & 0 deletions src/ui/pages/palette.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {gecutContext} from '@gecut/lit-helper/directives/context.js';
import {html} from 'lit/html.js';

import {paletteContext} from '../../signal/palette.context.js';
import {paletteReferenceColors} from '../components/palette-reference-colors.js';
import {paletteSystemColors} from '../components/palette-system-colors.js';

export function palettePage() {
return html`
<div class="flex flex-col gap-4 py-4">
<h1 class="text-headlineSmall text-onSurface">System Colors</h1>
${gecutContext(paletteContext, (theme) => paletteSystemColors(theme))}
<h1 class="text-headlineSmall text-onSurface">Reference Colors</h1>
${paletteReferenceColors()}
</div>
`;
}
Loading

0 comments on commit d3cb013

Please sign in to comment.