fix(in-app-menu): fix app crash in production

This commit is contained in:
Su-Yong
2024-02-05 23:14:00 +09:00
parent b3c05c8647
commit febc63edef
7 changed files with 143 additions and 132 deletions

View File

@ -1,5 +1,6 @@
import { createSignal } from 'solid-js'; import { createSignal } from 'solid-js';
import { render } from 'solid-js/web'; import { render } from 'solid-js/web';
import { extractCss } from 'solid-styled-components';
import { TitleBar } from './renderer/TitleBar'; import { TitleBar } from './renderer/TitleBar';
import { defaultInAppMenuConfig, InAppMenuConfig } from './constants'; import { defaultInAppMenuConfig, InAppMenuConfig } from './constants';

View File

@ -1,7 +1,9 @@
import { JSX } from 'solid-js'; import { JSX } from 'solid-js';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
const iconButton = css` import { cache } from '@/providers/decorators';
const iconButton = cache(() => css`
background: transparent; background: transparent;
width: 24px; width: 24px;
@ -28,12 +30,12 @@ const iconButton = css`
&:active { &:active {
scale: 0.9; scale: 0.9;
} }
`; `);
type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>; type CollapseIconButtonProps = JSX.HTMLAttributes<HTMLButtonElement>;
export const IconButton = (props: CollapseIconButtonProps) => { export const IconButton = (props: CollapseIconButtonProps) => {
return ( return (
<button {...props} class={iconButton}> <button {...props} class={iconButton()}>
{props.children} {props.children}
</button> </button>
); );

View File

@ -1,7 +1,8 @@
import { JSX, splitProps } from 'solid-js'; import { JSX, splitProps } from 'solid-js';
import { css } from 'solid-styled-components'; import { css } from 'solid-styled-components';
import { cache } from '@/providers/decorators';
const menuStyle = css` const menuStyle = cache(() => css`
-webkit-app-region: none; -webkit-app-region: none;
display: flex; display: flex;
@ -25,7 +26,7 @@ const menuStyle = css`
&[data-selected="true"] { &[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
} }
`; `);
export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & { export type MenuButtonProps = JSX.HTMLAttributes<HTMLLIElement> & {
text?: string; text?: string;
@ -35,7 +36,7 @@ export const MenuButton = (props: MenuButtonProps) => {
const [local, leftProps] = splitProps(props, ['text']); const [local, leftProps] = splitProps(props, ['text']);
return ( return (
<li {...leftProps} class={menuStyle} data-selected={props.selected}> <li {...leftProps} class={menuStyle()} data-selected={props.selected}>
{local.text} {local.text}
</li> </li>
); );

View File

@ -4,8 +4,9 @@ import { css } from 'solid-styled-components';
import { Transition } from 'solid-transition-group'; import { Transition } from 'solid-transition-group';
import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom'; import { autoUpdate, flip, offset, OffsetOptions, size } from '@floating-ui/dom';
import { useFloating } from 'solid-floating-ui'; import { useFloating } from 'solid-floating-ui';
import { cache } from '@/providers/decorators';
const panelStyle = css` const panelStyle = cache(() => css`
position: fixed; position: fixed;
top: var(--offset-y, 0); top: var(--offset-y, 0);
left: var(--offset-x, 0); left: var(--offset-x, 0);
@ -32,9 +33,9 @@ const panelStyle = css`
0 2px 8px rgba(0, 0, 0, 0.2); 0 2px 8px rgba(0, 0, 0, 0.2);
transform-origin: var(--origin-x, 50%) var(--origin-y, 50%); transform-origin: var(--origin-x, 50%) var(--origin-y, 50%);
`; `);
const animationStyle = { const animationStyle = cache(() => ({
enter: css` enter: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
@ -49,7 +50,7 @@ const animationStyle = {
exitActive: css` exitActive: css`
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0); transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`, `,
}; }));
export type Placement = export type Placement =
'top' 'top'
@ -122,16 +123,16 @@ export const Panel = (props: PanelProps) => {
<Portal> <Portal>
<Transition <Transition
appear appear
enterClass={animationStyle.enter} enterClass={animationStyle().enter}
enterActiveClass={animationStyle.enterActive} enterActiveClass={animationStyle().enterActive}
exitToClass={animationStyle.exitTo} exitToClass={animationStyle().exitTo}
exitActiveClass={animationStyle.exitActive} exitActiveClass={animationStyle().exitActive}
> >
<Show when={local.open}> <Show when={local.open}>
<ul <ul
{...leftProps} {...leftProps}
ref={setPanel} ref={setPanel}
class={panelStyle} class={panelStyle()}
style={{ style={{
'--offset-x': `${position.x}px`, '--offset-x': `${position.x}px`,
'--offset-y': `${position.y}px`, '--offset-y': `${position.y}px`,

View File

@ -8,117 +8,119 @@ import { useFloating } from 'solid-floating-ui';
import { autoUpdate, offset, size } from '@floating-ui/dom'; import { autoUpdate, offset, size } from '@floating-ui/dom';
import { Panel } from './Panel'; import { Panel } from './Panel';
import { cache } from '@/providers/decorators';
const itemStyle = css` const itemStyle = cache(() => css`
position: relative; position: relative;
-webkit-app-region: none; -webkit-app-region: none;
min-height: 32px; min-height: 32px;
height: 32px; height: 32px;
display: grid; display: grid;
grid-template-columns: 32px 1fr auto minmax(32px, auto); grid-template-columns: 32px 1fr auto minmax(32px, auto);
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
box-sizing: border-box; box-sizing: border-box;
user-select: none; user-select: none;
-webkit-user-drag: none; -webkit-user-drag: none;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
&:hover { &:hover {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
} }
&:active { &:active {
background-color: rgba(255, 255, 255, 0.2);
}
&[data-selected="true"] {
background-color: rgba(255, 255, 255, 0.2);
}
& * {
box-sizing: border-box;
}
`;
const itemIconStyle = css`
height: 32px;
padding: 4px;
color: white;
`;
const itemLabelStyle = css`
font-size: 12px;
color: white;
`;
const itemChipStyle = css`
display: flex;
justify-content: center;
align-items: center;
min-width: 16px;
height: 16px;
padding: 0 4px;
margin-left: 8px;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
color: #f1f1f1; }
font-size: 10px;
font-weight: 500;
line-height: 1;
`;
const toolTipStyle = css` &[data-selected="true"] {
min-width: 32px; background-color: rgba(255, 255, 255, 0.2);
width: 100%; }
height: 100%;
padding: 4px; & * {
box-sizing: border-box;
}
`);
max-width: calc(var(--max-width, 100%) - 8px); const itemIconStyle = cache(() => css`
max-height: calc(var(--max-height, 100%) - 8px); height: 32px;
padding: 4px;
color: white;
`);
border-radius: 4px; const itemLabelStyle = cache(() => css`
background-color: rgba(25, 25, 25, 0.8); font-size: 12px;
color: #f1f1f1; color: white;
font-size: 10px; `);
`;
const popupStyle = css` const itemChipStyle = cache(() => css`
position: fixed; display: flex;
top: var(--offset-y, 0); justify-content: center;
left: var(--offset-x, 0); align-items: center;
max-width: var(--max-width, 100%); min-width: 16px;
max-height: var(--max-height, 100%); height: 16px;
padding: 0 4px;
margin-left: 8px;
z-index: 100000000; border-radius: 4px;
pointer-events: none; background-color: rgba(255, 255, 255, 0.2);
color: #f1f1f1;
font-size: 10px;
font-weight: 500;
line-height: 1;
`);
`; const toolTipStyle = cache(() => css`
const animationStyle = { min-width: 32px;
width: 100%;
height: 100%;
padding: 4px;
max-width: calc(var(--max-width, 100%) - 8px);
max-height: calc(var(--max-height, 100%) - 8px);
border-radius: 4px;
background-color: rgba(25, 25, 25, 0.8);
color: #f1f1f1;
font-size: 10px;
`);
const popupStyle = cache(() => css`
position: fixed;
top: var(--offset-y, 0);
left: var(--offset-x, 0);
max-width: var(--max-width, 100%);
max-height: var(--max-height, 100%);
z-index: 100000000;
pointer-events: none;
`);
const animationStyle = cache(() => ({
enter: css` enter: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
`, `,
enterActive: css` enterActive: css`
transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1); transition: opacity 0.225s cubic-bezier(0.33, 1, 0.68, 1), transform 0.225s cubic-bezier(0.33, 1, 0.68, 1);
`, `,
exitTo: css` exitTo: css`
opacity: 0; opacity: 0;
transform: scale(0.9); transform: scale(0.9);
`, `,
exitActive: css` exitActive: css`
transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0); transition: opacity 0.225s cubic-bezier(0.32, 0, 0.67, 0), transform 0.225s cubic-bezier(0.32, 0, 0.67, 0);
`, `,
}; }));
const getParents = (element: Element | null): (HTMLElement | null)[] => { const getParents = (element: Element | null): (HTMLElement | null)[] => {
const parents: (HTMLElement | null)[] = []; const parents: (HTMLElement | null)[] = [];
@ -211,7 +213,7 @@ export const PanelItem = (props: PanelItemProps) => {
const closestLevel = parents.find((it) => it?.dataset?.level)?.dataset.level ?? ''; const closestLevel = parents.find((it) => it?.dataset?.level)?.dataset.level ?? '';
const path = event.composedPath(); const path = event.composedPath();
const isOtherItem = path.some((it) => it instanceof HTMLElement && it.classList.contains(itemStyle)); const isOtherItem = path.some((it) => it instanceof HTMLElement && it.classList.contains(itemStyle()));
const isChild = closestLevel.startsWith(props.level.join('/')); const isChild = closestLevel.startsWith(props.level.join('/'));
if (isOtherItem && !isChild) { if (isOtherItem && !isChild) {
@ -246,14 +248,14 @@ export const PanelItem = (props: PanelItemProps) => {
return ( return (
<li <li
ref={setAnchor} ref={setAnchor}
class={itemStyle} class={itemStyle()}
onMouseEnter={handleHover} onMouseEnter={handleHover}
onClick={handleClick} onClick={handleClick}
data-selected={open()} data-selected={open()}
> >
<Switch fallback={<div class={itemIconStyle}/>}> <Switch fallback={<div class={itemIconStyle()}/>}>
<Match when={props.type === 'checkbox' && props.checked}> <Match when={props.type === 'checkbox' && props.checked}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" <svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke="currentColor" fill="none"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/> <path stroke="none" d="M0 0h24v24H0z" fill="none"/>
@ -261,28 +263,30 @@ export const PanelItem = (props: PanelItemProps) => {
</svg> </svg>
</Match> </Match>
<Match when={props.type === 'radio' && props.checked}> <Match when={props.type === 'radio' && props.checked}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style={{ padding: '6px' }}> <svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
style={{ padding: '6px' }}>
<path fill="currentColor" <path fill="currentColor"
d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/> d="M10,5 C7.2,5 5,7.2 5,10 C5,12.8 7.2,15 10,15 C12.8,15 15,12.8 15,10 C15,7.2 12.8,5 10,5 L10,5 Z M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/>
</svg> </svg>
</Match> </Match>
<Match when={props.type === 'radio' && !props.checked}> <Match when={props.type === 'radio' && !props.checked}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" style={{ padding: '6px' }}> <svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
style={{ padding: '6px' }}>
<path fill="currentColor" <path fill="currentColor"
d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/> d="M10,0 C4.5,0 0,4.5 0,10 C0,15.5 4.5,20 10,20 C15.5,20 20,15.5 20,10 C20,4.5 15.5,0 10,0 L10,0 Z M10,18 C5.6,18 2,14.4 2,10 C2,5.6 5.6,2 10,2 C14.4,2 18,5.6 18,10 C18,14.4 14.4,18 10,18 L10,18 Z"/>
</svg> </svg>
</Match> </Match>
</Switch> </Switch>
<span class={itemLabelStyle}> <span class={itemLabelStyle()}>
{props.name} {props.name}
</span> </span>
<Show when={props.chip} fallback={<div/>}> <Show when={props.chip} fallback={<div/>}>
<span class={itemChipStyle}> <span class={itemChipStyle()}>
{props.chip} {props.chip}
</span> </span>
</Show> </Show>
<Show when={props.type === 'submenu'}> <Show when={props.type === 'submenu'}>
<svg class={itemIconStyle} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5" <svg class={itemIconStyle()} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
stroke-linecap="round" stroke-linejoin="round"> stroke-linecap="round" stroke-linejoin="round">
@ -304,7 +308,7 @@ export const PanelItem = (props: PanelItemProps) => {
<Portal> <Portal>
<div <div
ref={setToolTip} ref={setToolTip}
class={popupStyle} class={popupStyle()}
style={{ style={{
'--offset-x': `${position.x}px`, '--offset-x': `${position.x}px`,
'--offset-y': `${position.y}px`, '--offset-y': `${position.y}px`,
@ -312,13 +316,13 @@ export const PanelItem = (props: PanelItemProps) => {
> >
<Transition <Transition
appear appear
enterClass={animationStyle.enter} enterClass={animationStyle().enter}
enterActiveClass={animationStyle.enterActive} enterActiveClass={animationStyle().enterActive}
exitToClass={animationStyle.exitTo} exitToClass={animationStyle().exitTo}
exitActiveClass={animationStyle.exitActive} exitActiveClass={animationStyle().exitActive}
> >
<Show when={toolTipOpen()}> <Show when={toolTipOpen()}>
<div class={toolTipStyle}> <div class={toolTipStyle()}>
{props.toolTip} {props.toolTip}
</div> </div>
</Show> </Show>

View File

@ -11,8 +11,9 @@ import { WindowController } from './WindowController';
import type { RendererContext } from '@/types/contexts'; import type { RendererContext } from '@/types/contexts';
import type { InAppMenuConfig } from '../constants'; import type { InAppMenuConfig } from '../constants';
import { cache } from '@/providers/decorators';
const titleStyle = css` const titleStyle = cache(() => css`
-webkit-app-region: drag; -webkit-app-region: drag;
box-sizing: border-box; box-sizing: border-box;
@ -41,17 +42,17 @@ const titleStyle = css`
&[data-macos="true"] { &[data-macos="true"] {
padding: 4px 4px 4px 74px; padding: 4px 4px 4px 74px;
} }
`; `);
const separatorStyle = css` const separatorStyle = cache(() => css`
min-height: 1px; min-height: 1px;
height: 1px; height: 1px;
margin: 4px 0; margin: 4px 0;
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
`; `);
const animationStyle = { const animationStyle = cache(() => ({
enter: css` enter: css`
opacity: 0; opacity: 0;
transform: translateX(-50%) scale(0.8); transform: translateX(-50%) scale(0.8);
@ -76,7 +77,7 @@ const animationStyle = {
fake: css` fake: css`
transition: all 0.00000000001s; transition: all 0.00000000001s;
`, `,
}; }));
export type PanelRendererProps = { export type PanelRendererProps = {
items: Electron.Menu['items']; items: Electron.Menu['items'];
@ -140,7 +141,7 @@ const PanelRenderer = (props: PanelRendererProps) => {
/> />
</Match> </Match>
<Match when={subItem().type === 'separator'}> <Match when={subItem().type === 'separator'}>
<hr class={separatorStyle}/> <hr class={separatorStyle()}/>
</Match> </Match>
</Switch> </Switch>
</Show> </Show>
@ -251,7 +252,7 @@ export const TitleBar = (props: TitleBarProps) => {
}); });
return ( return (
<nav class={titleStyle} data-macos={props.isMacOS}> <nav class={titleStyle()} data-macos={props.isMacOS}>
<IconButton <IconButton
onClick={() => setCollapsed(!collapsed())} onClick={() => setCollapsed(!collapsed())}
style={{ style={{
@ -266,10 +267,10 @@ export const TitleBar = (props: TitleBarProps) => {
</svg> </svg>
</IconButton> </IconButton>
<TransitionGroup <TransitionGroup
enterClass={ignoreTransition() ? animationStyle.fakeTarget : animationStyle.enter} enterClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().enter}
enterActiveClass={ignoreTransition() ? animationStyle.fake : animationStyle.enterActive} enterActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().enterActive}
exitToClass={ignoreTransition() ? animationStyle.fakeTarget : animationStyle.exitTo} exitToClass={ignoreTransition() ? animationStyle().fakeTarget : animationStyle().exitTo}
exitActiveClass={ignoreTransition() ? animationStyle.fake : animationStyle.exitActive} exitActiveClass={ignoreTransition() ? animationStyle().fake : animationStyle().exitActive}
onBeforeEnter={(element) => { onBeforeEnter={(element) => {
if (ignoreTransition()) return; if (ignoreTransition()) return;
const index = Number(element.getAttribute('data-index') ?? 0); const index = Number(element.getAttribute('data-index') ?? 0);

View File

@ -2,8 +2,9 @@ import { css } from 'solid-styled-components';
import { Show } from 'solid-js'; import { Show } from 'solid-js';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { cache } from '@/providers/decorators';
const containerStyle = css` const containerStyle = cache(() => css`
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
@ -15,7 +16,7 @@ const containerStyle = css`
background: rgba(255, 0, 0, 0.5); background: rgba(255, 0, 0, 0.5);
} }
} }
`; `);
export type WindowControllerProps = { export type WindowControllerProps = {
isMaximize?: boolean; isMaximize?: boolean;
@ -26,7 +27,7 @@ export type WindowControllerProps = {
} }
export const WindowController = (props: WindowControllerProps) => { export const WindowController = (props: WindowControllerProps) => {
return ( return (
<div class={containerStyle}> <div class={containerStyle()}>
<IconButton onClick={props.onMinimize}> <IconButton onClick={props.onMinimize}>
<svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg width={16} height={16} fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="currentColor" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"/> <path fill="currentColor" d="M3.755 12.5h16.492a.75.75 0 0 0 0-1.5H3.755a.75.75 0 0 0 0 1.5Z"/>