Skip to content

Commit

Permalink
Merge pull request #258 from riccardoperra/90-gradient-as-theme-frame…
Browse files Browse the repository at this point in the history
…-background-color

feat: add theme gradients
riccardoperra authored May 22, 2022
2 parents 64bf8b2 + 0e93d04 commit 1124d1a
Showing 42 changed files with 998 additions and 73 deletions.
8 changes: 8 additions & 0 deletions .changeset/shiny-hounds-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@codeimage/app': minor
'@codeimage/config': minor
'@codeimage/theme': minor
'@codeimage/ui': minor
---

feat: add support for gradient colors
2 changes: 1 addition & 1 deletion apps/codeimage/package.json
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
"version": "0.17.2",
"scripts": {
"start": "vite",
"dev": "vite --host",
"dev": "vite --host --force",
"build": "rimraf dist && vite build",
"build-sw": "rimraf dist && cross-env BASE_URL=/ SW=true SOURCE_MAP=true RELOAD_SW=true vite build",
"serve:https": "serve dist",
6 changes: 4 additions & 2 deletions apps/codeimage/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {useI18n} from '@codeimage/locale';
import {connectStoreWithQueryParams} from '@codeimage/store/plugins/connect-store-with-query-params';
import {copyToClipboard$} from '@codeimage/store/effects/onCopyToClipboard';
import {onTabNameChange$} from '@codeimage/store/effects/onTabNameChange';
import {onThemeChange$} from '@codeimage/store/effects/onThemeChange';
import {frame$, setScale} from '@codeimage/store/frame';
import {connectStoreWithQueryParams} from '@codeimage/store/plugins/connect-store-with-query-params';
import {setTabName, terminal$} from '@codeimage/store/terminal';
import {Box, PortalHost, SnackbarHost} from '@codeimage/ui';
import {initEffects} from '@ngneat/effects';
@@ -15,6 +15,7 @@ import {Frame} from './components/Frame/Frame';
import {FrameHandler} from './components/Frame/FrameHandler';
import {KeyboardShortcuts} from './components/KeyboardShortcuts/KeyboardShortcuts';
import {EditorSidebar} from './components/PropertyEditor/EditorSidebar';
import {SidebarPopoverHost} from './components/PropertyEditor/SidebarPopoverHost';
import {Canvas} from './components/Scaffold/Canvas/Canvas';
import {Scaffold} from './components/Scaffold/Scaffold';
import {Sidebar} from './components/Scaffold/Sidebar/Sidebar';
@@ -45,6 +46,8 @@ const App = () => {

return (
<Scaffold>
<PortalHost id={'floating-portal-popover'} />
<SidebarPopoverHost />
<SnackbarHost />

<Show when={modality === 'full'}>
@@ -57,7 +60,6 @@ const App = () => {

<Canvas>
<Toolbar canvasRef={frameRef()} />

<Show when={modality === 'full'}>
<Box paddingLeft={4} paddingTop={3}>
<KeyboardShortcuts />
26 changes: 15 additions & 11 deletions apps/codeimage/src/components/PropertyEditor/FrameStyleForm.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import {PanelHeader} from './PanelHeader';
import {PanelRow, TwoColumnPanelRow} from './PanelRow';
import {ColorPicker, RangeField, SegmentedField} from '@codeimage/ui';
import {Show} from 'solid-js';
import {useI18n} from '@codeimage/locale';
import {
frame$,
setBackground,
setOpacity,
setVisibility,
} from '@codeimage/store/frame';
import {fromObservableObject} from '../../core/hooks/from-observable-object';
import {useI18n} from '@codeimage/locale';
import {setPadding} from '../../state/frame';
import {RangeField, SegmentedField} from '@codeimage/ui';
import {Show} from 'solid-js';
import {appEnvironment} from '../../core/configuration';
import {fromObservableObject} from '../../core/hooks/from-observable-object';
import {AppLocaleEntries} from '../../i18n';
import {setPadding} from '../../state/frame';
import {CustomColorPicker} from './controls/CustomColorPicker';
import {PanelHeader} from './PanelHeader';
import {PanelRow, TwoColumnPanelRow} from './PanelRow';

export const FrameStyleForm = () => {
const [t] = useI18n<AppLocaleEntries>();
@@ -71,10 +72,13 @@ export const FrameStyleForm = () => {
<Show when={frame.visible}>
<PanelRow for={'colorField'} label={t('frame.color')}>
<TwoColumnPanelRow>
<ColorPicker
id={'colorField'}
onChange={setBackground}
value={frame.background ?? undefined}
<CustomColorPicker
title={'Color'}
onChange={color => {
console.log(color);
setBackground(color);
}}
value={frame.background ?? ''}
/>
</TwoColumnPanelRow>
</PanelRow>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {themeVars} from '@codeimage/ui';
import {style} from '@vanilla-extract/css';
import {scaffoldVars} from '../Scaffold/Scaffold.css';

export const wrapper = style({
marginLeft: `calc(${scaffoldVars.panelWidth} + 10px)`,
position: 'absolute',
maxWidth: scaffoldVars.panelWidth,
height: '100%',
width: '100%',
zIndex: themeVars.zIndex['50'],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as styles from './SidebarPopoverHost.css';

export const SIDEBAR_POPOVER_HOST_ID = 'sidebar-popover-host';

export function SidebarPopoverHost() {
return <div class={styles.wrapper} id={SIDEBAR_POPOVER_HOST_ID} />;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {AVAILABLE_COLORS, AVAILABLE_GRADIENTS} from '@codeimage/config';
import {ColorPicker} from '@codeimage/ui';
import {Middleware} from '@floating-ui/dom';
import {SIDEBAR_POPOVER_HOST_ID} from '../SidebarPopoverHost';

export type CustomColorPickerProps = Parameters<typeof ColorPicker>[0];

export function CustomColorPicker(props: CustomColorPickerProps) {
const resetFloatingHorizontalSpace: Middleware = {
name: 'reset-h-space',
fn: () => ({x: 0}),
};

return (
<ColorPicker
colors={AVAILABLE_COLORS}
gradients={AVAILABLE_GRADIENTS}
popoverRoot={SIDEBAR_POPOVER_HOST_ID}
popoverPlacement={'right-start'}
popoverMiddlewares={[resetFloatingHorizontalSpace]}
{...props}
/>
);
}
2 changes: 1 addition & 1 deletion apps/codeimage/src/components/Toolbar/Toolbar.css.ts
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ export const wrapper = style([
color: themeVars.textColor.blue['900'],
fontSize: '18px',
fontWeight: 'bold',
zIndex: 30,
zIndex: 60,
height: toolbarVars.toolbarHeight,
width: '100%',
backgroundColor: themeVars.dynamicColors.panel.background,
4 changes: 4 additions & 0 deletions apps/codeimage/src/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sidebarLocale from './sidebar';
import {uiLocale} from './ui';

export const locale = {
it: {
@@ -72,6 +73,7 @@ export const locale = {
search: 'Cerca tema...',
},
...sidebarLocale.it,
...uiLocale.it,
},
en: {
...sidebarLocale.en,
@@ -142,6 +144,7 @@ export const locale = {
themeSwitcher: {
search: 'Search theme...',
},
...uiLocale.en,
},
de: {
common: {
@@ -212,6 +215,7 @@ export const locale = {
search: 'Thema suchen...',
},
...sidebarLocale.de,
...uiLocale.de,
},
};

20 changes: 20 additions & 0 deletions apps/codeimage/src/i18n/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const uiLocale = {
it: {
colorPicker: {
color: 'Colore',
gradient: 'Gradiente',
},
},
en: {
colorPicker: {
color: 'Color',
gradient: 'Gradient',
},
},
de: {
colorPicker: {
color: 'Color',
gradient: 'Gradient',
},
},
};
64 changes: 64 additions & 0 deletions packages/config/src/lib/base/colors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export const AVAILABLE_COLORS = [
'#000000',
'#1a1a1a',
'#1e222f',
'#213043',
'#383c4a',
'#999999',
'#A2B1D2',
'#FFFFFF',
'#da4d4d',
'#e97171',
'#FF88AA',
'#fdad5d',
'#e3db2a',
'#FFEE66',
'#ffcc99',
'#7e3bdf',
'#8663ed',
'#BD93F9',
'#d4b8ff',
'#79a1ff',
'#4455BB',
'#1e88df',
'#1b55d9',
'#1a3f95',
'#1c933f',
'#64e7a3',
'#21d5b8',
'#06535d',
];

export const AVAILABLE_GRADIENTS = [
// Purple
'linear-gradient(-45deg, #402662 0%, #8000FF 100%)',
'linear-gradient(135deg, #6a3cc0 0%, #240573 100%)',
'linear-gradient(to right top, #7f469d, #8242aa, #833db7, #8338c4, #8233d2, #8a35da, #9336e2, #9b38ea, #af41ee, #c24af2, #d554f7, #e65ffb)',
'linear-gradient(135deg, rgba(171,73,222,1) 0%,rgba(73,84,222,1) 100%)',
// Blue
'linear-gradient(to right top, #3547be, #285acd, #186ddb, #047ee7, #0090f2, #0091f3, #0091f4, #0092f5, #0082ec, #0071e2, #0060d7, #174ecb)',
'linear-gradient(135deg, rgba(73,84,222,1) 0%,rgba(73,221,216,1) 100%)',
'linear-gradient(135deg, #54D2EF 0%, #2AA6DA 100%)',
'linear-gradient(135deg, #2AA6DA 0%, #1B7B77 100%)',
// Green
'linear-gradient(to right bottom, #1e737e, #186b76, #13636d, #0d5b65, #06535d)',
'linear-gradient(to right bottom, #00ffc7, #00c6a5, #148f7e, #205b55, #1e2c2b)',
'linear-gradient(to right bottom, #2be7b5, #1edea2, #16d58f, #13cb7c, #16c268, #0db866, #04ae64, #00a462, #00976c, #008971, #007b72, #006d6d)',
'linear-gradient(135deg, #33CC99 0%, #FFCC33 100%)',
'linear-gradient(135deg, rgba(73,221,216,1) 0%,rgba(25,226,115,1) 100%)',
// Red
'linear-gradient(135deg, #FFE174 0%, #FFBF40 100%)',
'linear-gradient(to right bottom, #ffcc99, #f6bd83, #edad6e, #e49e59, #da8f44)',
'linear-gradient(to right bottom, #f66283, #e45475, #d34768, #c2395b, #b12a4e, #a92246, #a1183e, #990d36, #970931, #95052b, #930226, #900020)',
'linear-gradient(135deg, #E233FF 0%, #FF6B00 100%)',
// Mix
'linear-gradient(135deg, #FF0076 0%, #590FB7 100%)',
'linear-gradient(to right bottom, #d44be1, #c945d7, #be3fcd, #b43ac3, #a934b9, #b330af, #bb2ca6, #c12a9c, #d6308f, #e73c83, #f34d77, #fb5f6d)',
'linear-gradient(-45deg, #402662 0%, #F59ABE 100%)',
// Light
'linear-gradient(62deg, #8EC5FC 0%, #E0C3FC 100%)',
'linear-gradient(to right bottom, #82aaff, #6c88cb, #566899, #3f4969, #292d3e)',
'linear-gradient(to right bottom, #e5e5e5, #d0cfd5, #bbbac5, #a5a6b6, #9092a7, #83869d, #777a93, #6a6f89, #646881, #5d6279, #575b71, #515569)',
'linear-gradient(135deg, #CECED8 0%, #FFFFFF 100%)',
'linear-gradient(to right bottom, #393939, #343435, #2f3030, #2b2b2c, #262727)',
];
1 change: 1 addition & 0 deletions packages/config/src/public-api.ts
Original file line number Diff line number Diff line change
@@ -7,3 +7,4 @@ export {SUPPORTED_LOCALES} from './lib/base/locales';
export {SUPPORTED_LANGUAGES} from './lib/base/languages';
export {SUPPORTED_THEMES} from './lib/base/themes';
export {EDITOR_BASE_SETUP} from './lib/base/editor-basic-setup';
export {AVAILABLE_COLORS, AVAILABLE_GRADIENTS} from './lib/base/colors';
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/arc-dark/arcDark.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import {tags as t} from '@lezer/highlight';
import {defineEditorTheme} from '../../core';

export const palette = {
background: '#21252bff',
background: '#0a0b0e',
gray: '#383c4a',
grayLight: '#a2a2a2',
pink: '#C586C0',
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/arc-dark/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const arcDarkTheme = createTheme({
properties: {
darkMode: true,
label: 'Arc Dark',
previewBackground: palette.gray,
previewBackground: `linear-gradient(to right bottom, #393939, #343435, #2f3030, #2b2b2c, #262727)`,
terminal: {
main: palette.background,
text: palette.grayLight,
5 changes: 3 additions & 2 deletions packages/theme/src/lib/themes/coldark/index.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ export const coldarkColdTheme = createTheme({
properties: {
darkMode: false,
label: 'Coldark Cold',
previewBackground: '#A2B1D2',
previewBackground: 'linear-gradient(-45deg, #402662 0%, #F59ABE 100%)',
terminal: {
main: '#e3eaf2',
text: '#111b27',
@@ -22,7 +22,8 @@ export const coldarkDarkTheme = createTheme({
properties: {
darkMode: true,
label: 'Coldark Dark',
previewBackground: '#213043',
previewBackground:
'linear-gradient(to left top, #162b46, #192c45, #1c2e45, #1e2f44, #213043)',
terminal: {
main: '#111b27',
text: palette.white,
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/dracula/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const draculaTheme = createTheme({
properties: {
darkMode: true,
label: 'Dracula',
previewBackground: palette.purple,
previewBackground: `linear-gradient(135deg, rgba(171,73,222,1) 0%,rgba(73,84,222,1) 100%)`,
terminal: {
main: palette.background,
text: palette.purple,
3 changes: 2 additions & 1 deletion packages/theme/src/lib/themes/duotone-sea/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,8 @@ export const duotoneSeaTheme = createTheme({
properties: {
darkMode: true,
label: 'Duotone Sea',
previewBackground: '#06535d',
previewBackground:
'linear-gradient(to right bottom, #1e737e, #186b76, #13636d, #0d5b65, #06535d)',
terminal: {
main: '#1D262F',
text: '#D6E9FF',
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/light/theme.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const lightTheme = createTheme({
properties: {
darkMode: false,
label: 'One light',
previewBackground: '#0d6985',
previewBackground: 'linear-gradient(62deg, #8EC5FC 0%, #E0C3FC 100%)',
terminal: {
main: '#ffffff',
text: '#000000',
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/material-light/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const materialLightTheme = createTheme({
properties: {
darkMode: false,
label: 'Material Light',
previewBackground: '#d4b8ff',
previewBackground: 'linear-gradient(135deg, #54D2EF 0%, #2AA6DA 100%)',
terminal: {
main: '#ffffff',
text: '#90a4ae',
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/material-ocean/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const materialOceanTheme = createTheme({
properties: {
darkMode: true,
label: 'Material Ocean',
previewBackground: palette.defaultAccent,
previewBackground: `linear-gradient(to right bottom, #2be7b5, #1edea2, #16d58f, #13cb7c, #16c268, #0db866, #04ae64, #00a462, #00976c, #008971, #007b72, #006d6d)`,
terminal: {
main: palette.backgroundAlt,
text: palette.white,
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/material-ocean/theme.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ export const palette = {
brown: '#916b53',
pink: '#ff9cac',
violet: '#bb80b3',
backgroundAlt: '#263238',
backgroundAlt: '#192429',
defaultAccent: '#80CBC4',
background: '#263238',
comments: '#546E7A',
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/material-palenight/index.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ export const materialPalenightTheme = createTheme({
label: 'Material Palenight',
previewBackground: '#1e222f',
terminal: {
main: '#292D3E',
main: '#24293B',
text: palette.gray,
},
},
3 changes: 2 additions & 1 deletion packages/theme/src/lib/themes/material-volcano/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,8 @@ export const materialVolcanoTheme = createTheme({
properties: {
darkMode: true,
label: 'Material Volcano',
previewBackground: '#AB5959',
previewBackground:
'linear-gradient(to right bottom, #f66283, #e45475, #d34768, #c2395b, #b12a4e, #a92246, #a1183e, #990d36, #970931, #95052b, #930226, #900020)',
terminal: {
main: palette.background,
text: palette.white,
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/night-owl/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const nightOwlTheme = createTheme({
properties: {
darkMode: true,
label: 'Night Owl',
previewBackground: '#15357F',
previewBackground: 'linear-gradient(135deg, #FFE174 0%, #FFBF40 100%)',
terminal: {
main: '#011627',
text: '#D0D8E5',
2 changes: 1 addition & 1 deletion packages/theme/src/lib/themes/synthwave84/index.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ export const synthwave84Theme = createTheme({
properties: {
darkMode: true,
label: "Synthwave '84",
previewBackground: `#7e4fce`,
previewBackground: `linear-gradient(to right top, #7f469d, #8242aa, #833db7, #8338c4, #8233d2, #8a35da, #9336e2, #9b38ea, #af41ee, #c24af2, #d554f7, #e65ffb)`,
terminal: {
main: palette.background,
text: palette.white,
3 changes: 2 additions & 1 deletion packages/theme/src/lib/themes/vscode-dark/index.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@ export const vsCodeDarkTheme = createTheme({
properties: {
darkMode: true,
label: 'VSCode Dark',
previewBackground: '#529DDA',
previewBackground:
'linear-gradient(to right top, #3547be, #285acd, #186ddb, #047ee7, #0090f2, #0091f3, #0091f4, #0092f5, #0082ec, #0071e2, #0060d7, #174ecb)',
terminal: {
main: palette.background,
text: '#FFF',
20 changes: 19 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -28,6 +28,13 @@
"@vanilla-extract/recipes": "^0.2.4",
"@vanilla-extract/dynamic": "^2.0.2",
"solid-headless": "^0.10.12",
"@solid-aria/interactions": "^0.0.5",
"@solid-aria/focus": "^0.0.7",
"@solid-aria/overlays": "^0.0.3",
"@solid-aria/utils": "^0.0.5",
"@solid-primitives/utils": "^2.0.1",
"@solid-primitives/refs": "^0.2.0",
"@solid-primitives/props": "^2.1.2",
"solid-use": "^0.3.3",
"solid-js": "^1.3.17",
"csstype": "^3.0.11",
@@ -36,6 +43,7 @@
"@floating-ui/core": "^0.6.2"
},
"dependencies": {
"@codeimage/locale": "workspace:*",
"@vanilla-extract/css": "^1.7.0",
"@vanilla-extract/sprinkles": "^1.4.0",
"@vanilla-extract/recipes": "^0.2.4",
@@ -46,7 +54,17 @@
"csstype": "^3.0.11",
"clsx": "^1.1.1",
"@floating-ui/dom": "^0.4.5",
"@floating-ui/core": "^0.6.2"
"@floating-ui/core": "^0.6.2",
"@solid-aria/interactions": "^0.0.5",
"@solid-aria/focus": "^0.0.7",
"@solid-aria/overlays": "^0.0.3",
"@solid-aria/utils": "^0.0.5",
"@solid-aria/dialog": "^0.0.2",
"@solid-aria/button": "^0.0.3",
"@solid-aria/i18n": "^0.0.6",
"@solid-primitives/utils": "^2.0.1",
"@solid-primitives/refs": "^0.2.0",
"@solid-primitives/props": "^2.1.2"
},
"devDependencies": {
"@types/node": "^17.0.31",
5 changes: 5 additions & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -2,3 +2,8 @@ export * from './lib/theme';
export * from './lib/primitives';
export * from './lib/hooks';
export * from './lib/tokens';

export {
createFloatingPortalNode,
FloatingPortal,
} from './lib/floating/floating-portal';
50 changes: 50 additions & 0 deletions packages/ui/src/lib/floating/floating-portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
createSignal,
mergeProps,
onMount,
PropsWithChildren,
Show,
} from 'solid-js';
import {Portal} from 'solid-js/web';

const DEFAULT_ID = 'floating-ui-root';

export interface CreateFloatingPortalNodeProps {
id?: string;
}

export const createFloatingPortalNode = (
props: CreateFloatingPortalNodeProps,
) => {
const [portal, setPortal] = createSignal<HTMLElement | null>(null);
const propsWithDefault = mergeProps({id: DEFAULT_ID}, props);

onMount(() => {
const rootNode = document.getElementById(propsWithDefault.id);
if (rootNode) {
setPortal(rootNode);
} else {
const element = document.createElement('div');
element.id = propsWithDefault.id;
setPortal(element);
}
});
return portal;
};

export interface FloatingPortalProps {
id?: string;
root?: HTMLElement | null;
}

export const FloatingPortal = (
props: PropsWithChildren<FloatingPortalProps>,
) => {
const portalNode = createFloatingPortalNode({id: props.id});

return (
<Show when={portalNode()}>
{node => <Portal mount={node}>{props.children}</Portal>}
</Show>
);
};
2 changes: 1 addition & 1 deletion packages/ui/src/lib/hooks/useFloating.tsx
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ type Data = Omit<ComputePositionReturn, 'x' | 'y'> & {
y: number | null;
};

type UseFloatingReturn = Data & {
export type UseFloatingReturn = Data & {
update: () => void;
setReference: (node: ReferenceElement | null) => void;
setFloating: (node: ReferenceElement | null) => void;
53 changes: 41 additions & 12 deletions packages/ui/src/lib/primitives/ColorPicker/ColorPicker.css.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,50 @@
import {style} from '@vanilla-extract/css';
import {baseField} from '../TextField/TextField.css';
import {themeVars} from '../../theme';
import {recipe, RecipeVariants} from '@vanilla-extract/recipes';
import {backgroundColorVar, themeVars} from '../../theme';
import * as textFieldStyles from '../TextField/TextField.css';

export const colorPicker = style([
baseField,
export const wrapper = style({
display: 'flex',
width: '100%',
});

export const input = style([
textFieldStyles.baseField,
{
padding: themeVars.spacing['1'],
flex: 1,
},
{
appearance: 'none',
]);

export const inputColor = style({
borderRadius: themeVars.borderRadius.md,
width: themeVars.width.full,
height: '100%',
background: backgroundColorVar,
});

selectors: {
'&::-webkit-color-swatch-wrapper': {},
'&::-webkit-color-swatch': {
border: 0,
borderRadius: themeVars.borderRadius.md,
export const colorGrid = style({
display: 'grid',
gap: themeVars.spacing['3'],
gridTemplateColumns: 'repeat(6, 1fr)',
});

export const colorItem = recipe({
base: {
borderRadius: themeVars.borderRadius.full,
cursor: 'pointer',
width: '28px',
height: '28px',
background: backgroundColorVar,
boxShadow: themeVars.boxShadow.md,
},
variants: {
active: {
true: {
boxShadow: themeVars.boxShadow.outline,
},
},
},
]);
});

export type ColorPickerColorItemProps = RecipeVariants<typeof colorItem>;
147 changes: 119 additions & 28 deletions packages/ui/src/lib/primitives/ColorPicker/ColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,126 @@
import {
DynamicProps,
WithRef,
} from 'solid-headless/dist/types/utils/dynamic-prop';
import {Component} from 'solid-js';
import {omitProps} from 'solid-use';
import {styled} from '../../utils';
import {autoPlacement, Middleware, Placement} from '@floating-ui/dom';
import {createButton} from '@solid-aria/button';
import {createOverlayTrigger, OverlayContainer} from '@solid-aria/overlays';
import {assignInlineVars} from '@vanilla-extract/dynamic';
import {PropsWithChildren, Show} from 'solid-js';
import {useFloating} from '../../hooks';
import {backgroundColorVar} from '../../theme';
import {Box} from '../Box';
import {Popover} from '../Popover';
import {createPopoverPortal} from '../Popover/create-popover-portal';
import * as styles from './ColorPicker.css';
import {ColorPickerColorItemProps} from './ColorPicker.css';
import {ColorPickerPopover} from './ColorPickerPopover';

type ColorPickerProps = {
value?: string;
onChange?: (value: string) => void;
} & WithRef<'input'> &
Omit<DynamicProps<'input'>, 'as' | 'ref' | 'onInput' | 'onChange' | 'value'>;
export interface ColorPickerProps {
value: string | undefined;
onChange: (value: string) => void;
title?: string;
colors?: string[];
gradients?: string[];
popoverPlacement?: Placement;
popoverRoot?: string;
popoverMiddlewares?: Middleware[];
}

export const ColorPicker: Component<ColorPickerProps> = props => {
function onChange(e: Event): void {
if (props.onChange) {
const target = e.target as HTMLInputElement;
props.onChange(target.value);
}
}
export function ColorPicker(props: PropsWithChildren<ColorPickerProps>) {
let triggerRef: HTMLButtonElement | undefined;

const {triggerProps, overlayProps, state} = createOverlayTrigger({
type: 'dialog',
});

const {buttonProps} = createButton(
{
onPress: () => state.open(),
},
() => triggerRef,
);

const portal = createPopoverPortal({id: props.popoverRoot});

const floating = useFloating({
strategy: 'absolute',
placement: 'right-start',
middleware: [
autoPlacement({
allowedPlacements: [
'right-start',
'top-start',
'bottom-start',
'left-start',
],
}),
...(props?.popoverMiddlewares ?? []),
],
});

return (
<>
<button
{...buttonProps()}
{...triggerProps()}
class={styles.input}
ref={ref => {
triggerRef = ref;
floating.setReference(ref);
}}
>
<div
class={styles.inputColor}
style={assignInlineVars({
[backgroundColorVar]: props.value ?? '#000000',
})}
/>
</button>

<Show when={state.isOpen()}>
<OverlayContainer portalContainer={portal()!}>
<Popover
{...overlayProps()}
ref={floating.setFloating}
title={props.title}
style={{
top: `${floating.y}px`,
left: `${floating.x}px`,
position: floating.strategy,
}}
isOpen={state.isOpen()}
onClose={state.close}
>
<ColorPickerPopover
value={props.value}
onChange={props.onChange}
colors={props.colors}
gradientColors={props.gradients}
/>
</Popover>
</OverlayContainer>
</Show>
</>
);
}

type ColorPickerPresetItemProps = {
title: string;
color: string;
onClick: (color: string) => void;
active: boolean;
} & ColorPickerColorItemProps;

export function ColorPickerPresetItem(
props: PropsWithChildren<ColorPickerPresetItemProps>,
) {
return (
<styled.input
component={'input'}
class={styles.colorPicker}
value={props.value}
onInput={onChange}
onChange={onChange}
type={'color'}
{...omitProps(props, ['class', 'type', 'value', 'onChange'])}
<Box
class={styles.colorItem({
active: props.active,
})}
title={props.title}
style={assignInlineVars({
[backgroundColorVar]: props.color,
})}
onClick={() => props.onClick(props.color)}
/>
);
};
}
109 changes: 109 additions & 0 deletions packages/ui/src/lib/primitives/ColorPicker/ColorPickerPopover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
Accessor,
createEffect,
createMemo,
createSignal,
For,
on,
onMount,
Show,
} from 'solid-js';
import {VStack} from '../Box';
import {FlexField} from '../Field';
import {SegmentedField, SegmentedFieldItem} from '../SegmentedField';
import {TextField} from '../TextField';
import {ColorPickerPresetItem} from './ColorPicker';
import * as styles from './ColorPicker.css';
import {useI18n} from '@codeimage/locale';

enum ColorPickerSelectionMode {
gradient = 'gradient',
color = 'color',
}

export interface ColorPickerPopoverProps {
value?: string | null;
onChange: (value: string) => void;
gradientColors?: string[];
colors?: string[];
}

export function ColorPickerPopover(props: ColorPickerPopoverProps) {
const [internalColor, setInternalColor] = createSignal<string>();
const [mode, setMode] = createSignal(ColorPickerSelectionMode.gradient);
const [, {tUnsafe}] = useI18n();
createEffect(on(() => props.value, setInternalColor));

const modalities: Accessor<SegmentedFieldItem<ColorPickerSelectionMode>[]> =
createMemo(() => [
{
label: tUnsafe('colorPicker.gradient'),
value: ColorPickerSelectionMode.gradient,
},
{
label: tUnsafe('colorPicker.color'),
value: ColorPickerSelectionMode.color,
},
]);

onMount(() => {
if (props.colors?.includes(props.value ?? '')) {
setMode(ColorPickerSelectionMode.color);
} else {
setMode(ColorPickerSelectionMode.gradient);
}
});

return (
<VStack spacing={'4'}>
<FlexField size={'md'}>
<SegmentedField
value={mode()}
onChange={setMode}
items={modalities()}
/>
</FlexField>

<FlexField size={'xs'}>
<TextField
size={'xs'}
type={'text'}
value={internalColor()}
onChange={value => props.onChange(value)}
tabIndex={-1}
/>
</FlexField>

<Show when={mode() === ColorPickerSelectionMode.color}>
<div class={styles.colorGrid}>
<For each={props.colors}>
{item => (
<ColorPickerPresetItem
color={item}
title={item}
active={props.value === item}
onClick={() => {
props.onChange(item);
}}
/>
)}
</For>
</div>
</Show>
<Show when={mode() === ColorPickerSelectionMode.gradient}>
<div class={styles.colorGrid}>
<For each={props.gradientColors}>
{item => (
<ColorPickerPresetItem
color={item}
title={item}
active={props.value === item}
onClick={() => props.onChange(item)}
/>
)}
</For>
</div>
</Show>
</VStack>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {style} from '@vanilla-extract/css';
import {baseField} from '../TextField/TextField.css';
import {themeVars} from '../../theme';

export const colorPicker = style([
baseField,
{
flex: 1,
},
{
appearance: 'none',

selectors: {
'&::-webkit-color-swatch-wrapper': {},
'&::-webkit-color-swatch': {
border: 0,
borderRadius: themeVars.borderRadius.md,
},
},
},
]);
35 changes: 35 additions & 0 deletions packages/ui/src/lib/primitives/ColorPicker/NativeColorPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
DynamicProps,
WithRef,
} from 'solid-headless/dist/types/utils/dynamic-prop';
import {Component} from 'solid-js';
import {omitProps} from 'solid-use';
import {styled} from '../../utils';
import * as styles from './NativeColorPicker.css';

type ColorPickerProps = {
value?: string;
onChange?: (value: string) => void;
} & WithRef<'input'> &
Omit<DynamicProps<'input'>, 'as' | 'ref' | 'onInput' | 'onChange' | 'value'>;

export const NativeColorPicker: Component<ColorPickerProps> = props => {
function onChange(e: Event): void {
if (props.onChange) {
const target = e.target as HTMLInputElement;
props.onChange(target.value);
}
}

return (
<styled.input
component={'input'}
class={styles.colorPicker}
value={props.value}
onInput={onChange}
onChange={onChange}
type={'color'}
{...omitProps(props, ['class', 'type', 'value', 'onChange'])}
/>
);
};
1 change: 1 addition & 0 deletions packages/ui/src/lib/primitives/ColorPicker/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './NativeColorPicker';
export * from './ColorPicker';
34 changes: 34 additions & 0 deletions packages/ui/src/lib/primitives/Popover/Popover.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {style} from '@vanilla-extract/css';
import {themeVars} from '../../theme';
import {fontSize, fontWeight} from '../Text/Text.css';

export const container = style({
overflow: 'hidden',
borderRadius: themeVars.borderRadius.lg,
backgroundColor: themeVars.dynamicColors.panel.background,
color: themeVars.dynamicColors.panel.textColor,
display: 'flex',
flexDirection: 'column',
width: '260px',
boxShadow: themeVars.dynamicColors.dialog.panelShadow,
maxWidth: '300px',

':focus-visible': {
outline: 'none',
boxShadow: themeVars.boxShadow.outline,
},
});

export const title = style([
fontSize.sm,
fontWeight.semibold,
{
padding: themeVars.spacing['4'],
marginTop: 0,
paddingBottom: 0,
},
]);

export const content = style({
padding: themeVars.spacing['4'],
});
73 changes: 73 additions & 0 deletions packages/ui/src/lib/primitives/Popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {createDialog} from '@solid-aria/dialog';
import {FocusScope} from '@solid-aria/focus';
import {
AriaOverlayProps,
createModal,
createOverlay,
DismissButton,
} from '@solid-aria/overlays';
import {combineProps} from '@solid-primitives/props';
import {mergeRefs} from '@solid-primitives/refs';
import {access} from '@solid-primitives/utils';
import {createMemo, JSX, Show, splitProps} from 'solid-js';
import * as styles from './Popover.css';

interface PopoverProps extends AriaOverlayProps {
ref: HTMLDivElement | ((ref: HTMLDivElement) => void) | undefined;
style?: JSX.CSSProperties | string;
class?: string;
title?: JSX.Element;
children?: JSX.Element;
}

export function Popover(props: PopoverProps) {
let ref: HTMLDivElement | undefined;

const [local, others] = splitProps(props, [
'ref',
'class',
'title',
'children',
'isOpen',
'onClose',
]);

// Handle interacting outside the dialog and pressing
// the Escape key to close the modal.
const {overlayProps} = createOverlay(
{
onClose: local.onClose,
isOpen: () => access(local.isOpen),
isDismissable: true,
},
() => ref,
);

// Hide content outside the modal from screen readers.
const {modalProps} = createModal();

// Get props for the dialog and its title
const {dialogProps, titleProps} = createDialog({}, () => ref);

const rootProps = createMemo(() => {
return combineProps(overlayProps(), dialogProps(), modalProps(), others);
});

return (
<FocusScope restoreFocus>
<div
{...rootProps()}
ref={mergeRefs(el => (ref = el), local.ref)}
class={styles.container}
>
<Show when={!!props.title}>
<h3 {...titleProps} class={styles.title}>
{props.title}
</h3>
<div class={styles.content}>{props.children}</div>
</Show>
<DismissButton onDismiss={local.onClose} />
</div>
</FocusScope>
);
}
18 changes: 18 additions & 0 deletions packages/ui/src/lib/primitives/Popover/create-popover-portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {mergeProps} from 'solid-js';
import {
createFloatingPortalNode,
CreateFloatingPortalNodeProps,
} from '../../floating/floating-portal';

export const DEFAULT_PORTAL_POPOVER_ID = 'floating-portal-popover';

export function createPopoverPortal(props?: CreateFloatingPortalNodeProps) {
const propsWithDefault = mergeProps(
{
id: DEFAULT_PORTAL_POPOVER_ID,
},
props,
);

return createFloatingPortalNode(propsWithDefault);
}
1 change: 1 addition & 0 deletions packages/ui/src/lib/primitives/Popover/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Popover} from './Popover';
4 changes: 3 additions & 1 deletion packages/ui/src/lib/primitives/index.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ export {TextField, textFieldStyles, type TextFieldProps} from './TextField';

export {RangeField, rangeFieldStyles} from './RangeField';

export {ColorPicker} from './ColorPicker';
export {NativeColorPicker, ColorPicker} from './ColorPicker';

export {Box, VStack, HStack} from './Box';

@@ -66,3 +66,5 @@ export {
export {Loading, type LoaderProps, LoadingOverlay} from './Loader';

export {Toggle} from './Toggle';

export {Popover} from './Popover';
288 changes: 288 additions & 0 deletions pnpm-lock.yaml

0 comments on commit 1124d1a

Please sign in to comment.