Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cheatsheet settings for modes, order, and theme #1276

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions cursorless-nx/apps/cheatsheet-local/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
<script id="cheatsheet-data"></script>
</head>
<body>
<script>
// Initialize dark mode. Inline script runs ASAP to prevent flashing.
const setting = localStorage.getItem('theme');
const system = window.matchMedia('(prefers-color-scheme: dark)').matches;

const darkMode = setting === null ? system : setting === 'dark';

if (darkMode) {
document.documentElement.classList.add('dark');
}
</script>
<div id="root"></div>
</body>
</html>
1 change: 1 addition & 0 deletions cursorless-nx/apps/cheatsheet-local/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ module.exports = {
extend: {},
},
plugins: [],
darkMode: 'class',
};
71 changes: 45 additions & 26 deletions cursorless-nx/libs/cheatsheet/src/lib/cheatsheet.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,65 @@
import * as React from 'react';
import React, { useState } from 'react';
import CheatsheetListComponent from './components/CheatsheetListComponent';
import CheatsheetLegendComponent from './components/CheatsheetLegendComponent';
import cheatsheetLegend from './cheatsheetLegend';
import Helmet from 'react-helmet';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons';
import { faCircleQuestion, faGear } from '@fortawesome/free-solid-svg-icons';
import CheatsheetNotesComponent from './components/CheatsheetNotesComponent';
import SmartLink from './components/SmartLink';
import { CheatsheetInfo } from './CheatsheetInfo';
import { SettingsProvider } from './components/Settings/SettingsContext';
import { DarkModeToggle } from './components/DarkModeToggle';
import { SettingsControls } from './components/Settings/SettingsControls';
import { IconButton } from './components/IconButton';

type CheatsheetPageProps = {
cheatsheetInfo: CheatsheetInfo;
};

// markup
export const CheatsheetPage: React.FC<CheatsheetPageProps> = ({
cheatsheetInfo,
}) => {
const [settingsOpen, setSettingsOpen] = useState(false);

return (
<main className="dark:text-stone-100">
<Helmet
bodyAttributes={{
class: 'bg-stone-50 dark:bg-stone-800',
}}
/>
<h1 className="text-2xl md:text-3xl text-center mt-2 mb-1 xl:mt-4">
Cursorless Cheatsheet{' '}
<span className="text-sm inline-block align-middle">
<SmartLink to="#legend">
<FontAwesomeIcon icon={faCircleQuestion} />
</SmartLink>
</span>
<small className="text-sm block">
See the{' '}
<SmartLink to={'https://www.cursorless.org/docs/'}>
full documentation
</SmartLink>{' '}
to learn more.
</small>
</h1>
<Cheatsheet cheatsheetInfo={cheatsheetInfo} />
</main>
<SettingsProvider>
<main className="dark:text-stone-100">
<Helmet
bodyAttributes={{
class: 'bg-stone-50 dark:bg-stone-800',
}}
/>
<div className="absolute flex gap-1 right-0 top-0 p-2">
<IconButton
icon={faGear}
title="Settings"
onClick={() => setSettingsOpen((v) => !v)}
/>
<DarkModeToggle />
</div>
<h1 className="text-2xl md:text-3xl text-center mt-2 mb-1 xl:mt-4">
Cursorless Cheatsheet{' '}
<span className="text-sm inline-block align-middle">
<SmartLink to="#legend">
<FontAwesomeIcon icon={faCircleQuestion} />
</SmartLink>
</span>
<small className="text-sm block">
See the{' '}
<SmartLink to={'https://www.cursorless.org/docs/'}>
full documentation
</SmartLink>{' '}
to learn more.
</small>
</h1>
<SettingsControls
onClose={() => setSettingsOpen(false)}
open={settingsOpen}
/>
<Cheatsheet cheatsheetInfo={cheatsheetInfo} />
</main>
</SettingsProvider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import * as React from 'react';
import React, { useEffect, useState } from 'react';
import useIsHighlighted from '../hooks/useIsHighlighted';
import { CheatsheetSection } from '../CheatsheetInfo';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faChevronUp } from '@fortawesome/free-solid-svg-icons';
import clsx from 'clsx';
import { useSettings } from './Settings/SettingsContext';

type Props = {
section: CheatsheetSection;
Expand All @@ -10,13 +14,35 @@ export default function CheatsheetListComponent({
section,
}: Props): JSX.Element {
const isHighlighted = useIsHighlighted(section.id);
const { modeConfig, ...settings } = useSettings();

const variations = section.items.flatMap((item) => item.variations);
const collapseItemIds = modeConfig.defaultCommands[section.id];
const collapsible = !!collapseItemIds;
const collapseDefault = collapsible && modeConfig.collapseByDefault;

variations.sort((form1, form2) =>
form1.spokenForm.localeCompare(form2.spokenForm)
const [collapsed, setCollapsed] = useState(collapseDefault);

useEffect(
() => setCollapsed(collapseDefault),
[modeConfig.collapseByDefault]
);

const toggleCollapse = () => setCollapsed((current) => !current);

const variations = section.items
.filter((item) => !collapsed || collapseItemIds?.includes(item.id))
.flatMap((item) => item.variations);

variations.sort((form1, form2) => {
switch (settings.order) {
case 'alphabetical':
return form1.spokenForm.localeCompare(form2.spokenForm);
case 'functional':
// TODO add more meaningful grouping to sort by
return form1.description.localeCompare(form2.description);
}
});

const borderClassName = isHighlighted
? 'border-violet-500 dark:border-violet-400'
: 'border-stone-300 dark:border-stone-500';
Expand All @@ -26,7 +52,29 @@ export default function CheatsheetListComponent({
id={section.id}
className={`border ${borderClassName} rounded-lg bg-stone-100 dark:bg-stone-700`}
>
<h2 className="text-xl text-center my-1">{section.name}</h2>
<h2 className="text-xl text-center">
<button
className={clsx(
'py-1 w-full flex justify-center items-center rounded-t-lg',
collapsible && 'hover:bg-stone-50 dark:hover:bg-stone-600'
)}
onClick={toggleCollapse}
disabled={!collapsible}
title={collapsed ? 'Show more' : 'Show less'}
>
{section.name}
{collapseItemIds != null && (
<div
className={clsx(
'ml-2 text-stone-500 dark:text-stone-400 text-xs transform transition-transform',
collapsed && 'rotate-180'
)}
>
<FontAwesomeIcon icon={faChevronUp} />
</div>
)}
</button>
</h2>
<table className="w-full">
<thead>
<tr className="text bg-stone-300 dark:bg-stone-500">
Expand All @@ -37,17 +85,19 @@ export default function CheatsheetListComponent({
</tr>
</thead>
<tbody>
{variations.map(({ spokenForm, description }) => (
{variations.map((variation) => (
<tr
key={spokenForm}
key={variation.spokenForm}
className="odd:bg-stone-200 dark:odd:bg-stone-600"
>
<td className="px-1">
<span className="text-stone-400">&#8220;</span>
{spokenForm}
{variation.spokenForm}
<span className="text-stone-400">&#8221;</span>
</td>
<td className="border-l border-stone-400 px-1">{description}</td>
<td className="border-l border-stone-400 px-1">
{variation.description}
</td>
</tr>
))}
</tbody>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { faMoon, faSun } from '@fortawesome/free-solid-svg-icons';
import { useSettings } from './Settings/SettingsContext';
import { IconButton } from './IconButton';

export const DarkModeToggle: React.FC = () => {
const settings = useSettings();

return (
<IconButton
onClick={() => settings.setTheme(settings.darkMode ? 'light' : 'dark')}
title={settings.darkMode ? 'Light mode' : 'Dark mode'}
icon={settings.darkMode ? faSun : faMoon}
/>
);
};
19 changes: 19 additions & 0 deletions cursorless-nx/libs/cheatsheet/src/lib/components/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import React from 'react';

export type IconButtonProps = {
icon: IconProp;
} & React.HTMLAttributes<HTMLButtonElement>;

export const IconButton: React.FC<IconButtonProps> = (props) => {
const { icon, className, ...buttonProps } = props;
return (
<button
{...buttonProps}
className={`${className} p-2 rounded-full aspect-square flex justify-center items-center hover:bg-black dark:hover:bg-white hover:bg-opacity-5 dark:hover:bg-opacity-10`}
>
<FontAwesomeIcon icon={props.icon} />
</button>
);
};
Loading