diff --git a/app/assets/stylesheets/components/buttons.scss b/app/assets/stylesheets/components/buttons.scss
index 2233c83c5..9cda36929 100644
--- a/app/assets/stylesheets/components/buttons.scss
+++ b/app/assets/stylesheets/components/buttons.scss
@@ -412,3 +412,106 @@ label[class*='crayons-btn']:focus-within // label for input[type="file"] made to
}
}
}
+
+////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+// .c-btn //////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+
+.c-btn {
+ position: relative;
+ display: inline-block;
+ padding: var(--su-2) var(--su-4);
+ border-radius: var(--radius);
+ font: inherit;
+ text-align: center;
+ cursor: pointer;
+ transition: all var(--transition-props);
+ border: none;
+ overflow-wrap: normal;
+ outline: 0;
+ background-color: var(--bg);
+ color: var(--color);
+
+ &:hover:enabled {
+ background-color: var(--bg-hover);
+ color: var(--color-hover);
+ z-index: var(--z-elevate);
+ }
+
+ // Both focus declarations below are the same but we unfortunately can't
+ // combine them because Safari doesn't handle it properly.
+ // • First declaration is for all browsers that support :focus-visible
+ // which is basically almost all of them.
+ // • Second declaration is for browsers that do not support :focus-visible
+ // which basically is only Safari right now :shrug:
+ &:focus-visible {
+ background-color: var(--bg-hover);
+ color: var(--color-hover);
+ z-index: var(--z-elevate);
+ box-shadow: var(--focus-ring);
+ }
+
+ .js-focus-visible &.focus-visible:focus {
+ background-color: var(--bg-hover);
+ color: var(--color-hover);
+ z-index: var(--z-elevate);
+ box-shadow: var(--focus-ring);
+ }
+
+ &[disabled] {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+
+ // This makes sure icons inside buttons don't cause problems with click handlers
+ // See: https://css-tricks.com/slightly-careful-sub-elements-clickable-things/
+ & > * {
+ pointer-events: none;
+ + input {
+ pointer-events: revert;
+ }
+ }
+}
+
+.c-btn {
+ --bg: var(--btn-bg);
+ --bg-hover: var(--btn-bg-hover);
+ --color: var(--btn-color);
+ --color-hover: var(--btn-color-hover);
+
+ &.c-btn--destructive {
+ --bg: var(--btn-destructive-bg);
+ --bg-hover: var(--btn-destructive-bg-hover);
+ --color: var(--btn-destructive-color);
+ --color-hover: var(--btn-destructive-color-hover);
+ }
+}
+
+.c-btn--primary {
+ --bg: var(--btn-primary-bg);
+ --bg-hover: var(--btn-primary-bg-hover);
+ --color: var(--btn-primary-color);
+ --color-hover: var(--btn-primary-color-hover);
+
+ font-weight: var(--fw-medium);
+
+ &.c-btn--destructive {
+ --bg: var(--btn-primary-destructive-bg);
+ --bg-hover: var(--btn-primary-destructive-bg-hover);
+ --color: var(--btn-primary-destructive-color);
+ --color-hover: var(--btn-primary-destructive-color-hover);
+ }
+}
+
+.c-btn--icon-alone {
+ padding: var(--su-2);
+}
+
+.c-btn--icon-left {
+ .c-btn__icon {
+ margin-right: var(--su-2);
+ margin-left: calc(var(--su-1) * -1);
+ }
+}
diff --git a/app/assets/stylesheets/components/ctas.scss b/app/assets/stylesheets/components/ctas.scss
new file mode 100644
index 000000000..0b11d4d97
--- /dev/null
+++ b/app/assets/stylesheets/components/ctas.scss
@@ -0,0 +1,40 @@
+////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+// .c-cta //////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+
+.c-link {
+ &.c-cta {
+ --border-width: 1px;
+ padding: calc(var(--su-2) - var(--border-width))
+ calc(var(--su-4) - var(--border-width));
+ border: var(--border-width) solid var(--border);
+ text-decoration: none;
+ background-color: var(--bg);
+ color: var(--color);
+ text-align: center;
+ display: inline-block;
+ }
+
+ &.c-cta {
+ --bg: transparent;
+ --bg-hover: var(--base-20);
+ --color: var(--base-80);
+ --color-hover: var(--base-100);
+ --border: var(--base-60);
+ --border-hover: var(--base-80);
+ }
+
+ &.c-cta--branded {
+ --bg: transparent;
+ --bg-hover: var(--accent-brand-a10);
+ --color: var(--accent-brand);
+ --color-hover: var(--accent-brand-darker);
+ --border: var(--accent-brand);
+ --border-hover: var(--accent-brand-darker);
+
+ font-weight: var(--fw-medium);
+ }
+
+}
diff --git a/app/assets/stylesheets/components/links.scss b/app/assets/stylesheets/components/links.scss
new file mode 100644
index 000000000..d54984cf9
--- /dev/null
+++ b/app/assets/stylesheets/components/links.scss
@@ -0,0 +1,93 @@
+////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+// .c-link /////////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+
+.c-link {
+ position: relative;
+ display: inline-block;
+ border-radius: var(--radius);
+ font: inherit;
+ cursor: pointer;
+ transition: all var(--transition-props);
+ outline: 0;
+ text-decoration: none;
+ color: var(--color);
+
+ &--block {
+ display: flex;
+ padding: var(--su-2) var(--su-4);
+ overflow-wrap: normal;
+ background-color: var(--bg);
+
+ &:hover {
+ background-color: var(--bg-hover);
+ }
+ }
+
+ &:hover {
+ color: var(--color-hover);
+ z-index: var(--z-elevate);
+ text-decoration: underline;
+ }
+
+ // Both focus declarations below are the same but we unfortunately can't
+ // combine them because Safari doesn't recognize it properly.
+ // • First declaration is for all browsers that support :focus-visible
+ // which is basically almost all of them
+ // • Second declaration is for browsers that do not support :focus-visible
+ // which basically is only Safari right now.
+ &:focus-visible {
+ color: var(--color-hover);
+ z-index: var(--z-elevate);
+ box-shadow: var(--focus-ring);
+ text-decoration: underline;
+ }
+
+ .js-focus-visible &.focus-visible:focus {
+ color: var(--color-hover);
+ z-index: var(--z-elevate);
+ box-shadow: var(--focus-ring);
+ text-decoration: underline;
+ }
+
+ &--block {
+ &:focus-visible {
+ background-color: var(--bg-hover);
+ }
+
+ .js-focus-visible &.focus-visible:focus {
+ background-color: var(--bg-hover);
+ }
+ }
+}
+
+.c-link {
+ --bg: transparent;
+ --bg-hover: var(--base-20);
+ --color: var(--base-80);
+ --color-hover: var(--accent-brand-darker);
+}
+
+.c-link--branded {
+ --bg: transparent;
+ --bg-hover: var(--accent-brand-a10);
+ --color: var(--accent-brand);
+ --color-hover: var(--accent-brand-darker);
+}
+
+.c-link--icon-alone {
+ padding: var(--su-2);
+ // If we ever want to display only with icon it should probably
+ // no longer be `block` (or `flex`) element, i.e. "Notifications" icon in
+ // the header. Hence reverting to `inline-block`.
+ display: inline-block;
+}
+
+.c-link--icon-left {
+ .c-link__icon {
+ margin-right: var(--su-2);
+ margin-left: calc(var(--su-1) * -1);
+ }
+}
diff --git a/app/assets/stylesheets/config/_colors.scss b/app/assets/stylesheets/config/_colors.scss
index d0f69c01d..e3ea934a7 100644
--- a/app/assets/stylesheets/config/_colors.scss
+++ b/app/assets/stylesheets/config/_colors.scss
@@ -69,9 +69,9 @@
--accent-warning-a10: #{rgba(#ffcf4c, 0.1)};
// Danger
- --accent-danger: #dc1818;
- --accent-danger-darker: #c20a0a;
- --accent-danger-lighter: #ec5050;
+ --accent-danger: #d31616;
+ --accent-danger-darker: #b90909;
+ --accent-danger-lighter: #ec4646;
--accent-danger-a10: #{rgba(#dc1818, 0.1)};
////////////////////////////////////////////////////////////////////////////////////
@@ -241,3 +241,37 @@
--reaction-comment-color: var(--accent-warning);
--reaction-comment-bg: var(--accent-warning-a10);
}
+
+:root {
+ // Separating some variables here as these are the results
+ // of broader Crayons refactoring (see #15311) and I didn't
+ // want to mix everything together.
+
+ ////////////////////////////////////////////////
+ // Buttons /////////////////////////////////////
+ ////////////////////////////////////////////////
+
+ // Buttons: Default
+ --btn-bg: transparent;
+ --btn-bg-hover: var(--base-20);
+ --btn-color: var(--base-80);
+ --btn-color-hover: var(--base-100);
+
+ // Buttons: Default Destructive
+ --btn-destructive-bg: transparent;
+ --btn-destructive-bg-hover: var(--accent-danger-a10);
+ --btn-destructive-color: var(--accent-danger);
+ --btn-destructive-color-hover: var(--accent-danger-darker);
+
+ // Buttons: Primary
+ --btn-primary-bg: var(--accent-brand);
+ --btn-primary-bg-hover: var(--accent-brand-darker);
+ --btn-primary-color: var(--base-inverted);
+ --btn-primary-color-hover: var(--base-inverted);
+
+ // Buttons: Primary Destructive
+ --btn-primary-destructive-bg: var(--accent-danger);
+ --btn-primary-destructive-bg-hover: var(--accent-danger-darker);
+ --btn-primary-destructive-color: var(--base-inverted);
+ --btn-primary-destructive-color-hover: var(--base-inverted);
+}
diff --git a/app/assets/stylesheets/config/_variables.scss b/app/assets/stylesheets/config/_variables.scss
index 48bdab50a..1a6c4d997 100644
--- a/app/assets/stylesheets/config/_variables.scss
+++ b/app/assets/stylesheets/config/_variables.scss
@@ -52,6 +52,13 @@
--transition-time: 100ms;
--transition-props: var(--transition-func) var(--transition-time);
+ ////////////////////////////////////////////////////////////////////////////////////
+ // Focus ring //////////////////////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////////////////////
+
+ --focus-ring: 0 0 0 2px var(--base-inverted),
+ 0 0 0 4px var(--button-primary-bg-hover); // TODO: replace with appropriate variable from _colors.scss
+
////////////////////////////////////////////////////////////////////////////////////
// Typography. /////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////
diff --git a/app/assets/stylesheets/crayons.scss b/app/assets/stylesheets/crayons.scss
index aa7759916..3c37f2f15 100644
--- a/app/assets/stylesheets/crayons.scss
+++ b/app/assets/stylesheets/crayons.scss
@@ -14,6 +14,7 @@
@import 'components/buttons';
@import 'components/cards';
@import 'components/comments';
+@import 'components/ctas';
@import 'components/dropdowns';
@import 'components/editor-toolbar';
@import 'components/forms';
@@ -21,6 +22,7 @@
@import 'components/header';
@import 'components/headers';
@import 'components/indicators';
+@import 'components/links';
@import 'components/loaders';
@import 'components/media-cards';
@import 'components/modals';
diff --git a/app/assets/stylesheets/themes/dark.scss b/app/assets/stylesheets/themes/dark.scss
index 8e4adc8fb..02879a063 100644
--- a/app/assets/stylesheets/themes/dark.scss
+++ b/app/assets/stylesheets/themes/dark.scss
@@ -65,10 +65,10 @@
--accent-warning-a10: #{rgba(#ffcf4c, 0.1)};
// Danger
- --accent-danger: #dc1818;
- --accent-danger-darker: #c20a0a;
- --accent-danger-lighter: #ec5050;
- --accent-danger-a10: #{rgba(#dc1818, 0.1)};
+ --accent-danger: #ed3434;
+ --accent-danger-darker: #e40a0a;
+ --accent-danger-lighter: #f15555;
+ --accent-danger-a10: #{rgba(#ed3434, 0.1)};
// Main colors
--body-bg: var(--base-0);
@@ -230,3 +230,19 @@
--reaction-save-color: var(--accent-brand);
--reaction-save-bg: var(--accent-brand-a10);
}
+
+:root {
+ // Separating some variables here as these are the results
+ // of broader Crayons refactoring (see #15311) and I didn't
+ // want to mix everything together.
+
+ ////////////////////////////////////////////////
+ // Buttons /////////////////////////////////////
+ ////////////////////////////////////////////////
+
+ // Buttons: Primary
+ --btn-primary-bg-hover: var(--accent-brand-lighter);
+
+ // Buttons: Primary Destructive
+ --btn-primary-destructive-bg-hover: var(--accent-danger-lighter);
+}
diff --git a/app/javascript/.storybook/main.js b/app/javascript/.storybook/main.js
index 732e940c0..ffe944fcc 100644
--- a/app/javascript/.storybook/main.js
+++ b/app/javascript/.storybook/main.js
@@ -11,6 +11,7 @@ module.exports = {
// https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#correct-globs-in-mainjs
stories: ['../**/__stories__/*.stories.@(mdx|jsx)'],
addons: [
+ '@storybook/addon-controls',
'@storybook/addon-knobs',
'@storybook/addon-actions',
'@storybook/addon-links',
@@ -81,4 +82,8 @@ module.exports = {
return config;
},
+ babel: async (options) => ({
+ ...options,
+ plugins: [...options.plugins, 'inline-react-svg'],
+ }),
};
diff --git a/app/javascript/.storybook/preview.js b/app/javascript/.storybook/preview.js
index 07a392fc9..3affda257 100644
--- a/app/javascript/.storybook/preview.js
+++ b/app/javascript/.storybook/preview.js
@@ -92,6 +92,7 @@ Foo.parameters = {
};
export const parameters = {
+ controls: { expanded: true },
options: {
storySort: {
order: [
diff --git a/app/javascript/.storybook/storybook.scss b/app/javascript/.storybook/storybook.scss
index 11f533715..380549805 100644
--- a/app/javascript/.storybook/storybook.scss
+++ b/app/javascript/.storybook/storybook.scss
@@ -6,4 +6,19 @@ body {
#root {
padding: 2rem;
}
+
+ .sbdocs-ul,
+ .sbdocs-ol {
+ list-style: revert;
+ margin: revert;
+ padding: revert;
+ }
+
+ code {
+ all: revert;
+ }
+
+ .docblock-source {
+ margin: revert;
+ }
}
diff --git a/app/javascript/actionsPanel/initializeActionsPanelToggle.js b/app/javascript/actionsPanel/initializeActionsPanelToggle.js
index 00d79b1c1..de54be59a 100644
--- a/app/javascript/actionsPanel/initializeActionsPanelToggle.js
+++ b/app/javascript/actionsPanel/initializeActionsPanelToggle.js
@@ -17,7 +17,9 @@ export function initializeActionsPanel(user, path) {
`;
function toggleModActionsMenu() {
- document.getElementById('mod-actions-menu-btn-area').classList.remove('hidden');
+ document
+ .getElementById('mod-actions-menu-btn-area')
+ .classList.remove('hidden');
document
.getElementsByClassName('mod-actions-menu')[0]
.classList.toggle('showing');
@@ -41,8 +43,8 @@ export function initializeActionsPanel(user, path) {
// eslint-disable-next-line no-restricted-globals
if (!isModerationPage()) {
// don't show mod button in mod center page
- document.getElementById('mod-actions-menu-btn-area')
- .innerHTML = modActionsMenuIconHTML
+ document.getElementById('mod-actions-menu-btn-area').innerHTML =
+ modActionsMenuIconHTML;
document
.getElementsByClassName('mod-actions-menu-btn')[0]
.addEventListener('click', toggleModActionsMenu);
diff --git a/app/javascript/crayons/Buttons/Button.jsx b/app/javascript/crayons/Buttons/Button.jsx
new file mode 100644
index 000000000..54e118193
--- /dev/null
+++ b/app/javascript/crayons/Buttons/Button.jsx
@@ -0,0 +1,84 @@
+import { h } from 'preact';
+import PropTypes from 'prop-types';
+import { useState } from 'preact/hooks';
+import classNames from 'classnames/bind';
+import { defaultChildrenPropTypes } from '../../common-prop-types/default-children-prop-types';
+import { Icon } from '@crayons';
+
+export const ButtonNew = (props) => {
+ const {
+ children,
+ primary,
+ icon,
+ rounded,
+ destructive,
+ type = 'button',
+ className,
+ tooltip,
+ onKeyUp,
+ ...otherProps
+ } = props;
+
+ const [suppressTooltip, setSuppressTooltip] = useState(false);
+
+ const handleKeyUp = (event) => {
+ onKeyUp?.(event);
+ if (!tooltip) {
+ return;
+ }
+ setSuppressTooltip(event.key === 'Escape');
+ };
+
+ const classes = classNames('c-btn', {
+ 'c-btn--primary': primary,
+ 'c-btn--destructive': destructive,
+ 'c-btn--icon-left': icon && children,
+ 'c-btn--icon-alone': icon && !children,
+ 'crayons-tooltip__activator': tooltip,
+ 'radius-full': rounded,
+ [className]: className,
+ });
+
+ return (
+
+ );
+};
+
+ButtonNew.displayName = 'ButtonNew';
+
+ButtonNew.propTypes = {
+ children: defaultChildrenPropTypes,
+ primary: PropTypes.bool,
+ rounded: PropTypes.bool,
+ destructive: PropTypes.bool,
+ type: PropTypes.oneOf(['button', 'submit']),
+ className: PropTypes.string,
+ tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ onKeyUp: PropTypes.func,
+ icon: PropTypes.ReactNode,
+};
diff --git a/app/javascript/crayons/Buttons/__stories__/Buttons.mdx b/app/javascript/crayons/Buttons/__stories__/Buttons.mdx
new file mode 100644
index 000000000..4ef137697
--- /dev/null
+++ b/app/javascript/crayons/Buttons/__stories__/Buttons.mdx
@@ -0,0 +1,63 @@
+# Buttons
+
+Buttons are clickable elements that are used to trigger actions.
+They communicate calls to action to the user and allow users to interact with pages in a variety of ways.
+Button labels express what action will occur when the user interacts with it.
+
+ It is therefore very important that the different variants are implemented consistently across product,
+ so that they message the correct actions.
+
+## CTAs vs Links vs Buttons
+
+The most important of these rules is that the `` (or ``) and `` elements are assigned to appropriate respective tasks.
+That is: `` or `` should be used where navigation is elicited, and `` where a non-navigational action is being invoked.
+For example, a 'Submit' control on 'Sign in' view (form) would suit a `` because its job is to submit the form.
+However, 'Register' call-to-action—taking the user to a different form, in a different page or screen—would benefit from `` or `` semantics.
+
+Do not use `s` as navigational elements. Instead, use Links (or CTAs) when the desired action is to take the user to a new page.
+
+## Variants
+
+### Primary
+
+Primary button has special look: high contrast (against rest of the view/component) and it's filled with accent color.
+
+Ideally there should be only one Primary button per entire component, or sometimes even per entire view.
+Primary buttons should be used for the principal call to action on the page.
+
+### Default
+
+Default variant is less prominent, and therefore can be used for less prominent actions.
+Default buttons are visually light (greyish) and you can have as many of them as you want (within common sense :D).
+They can (but don't have to) be used in conjuction with a Primary button. In that case, default variant should be used for any remaining actions.
+
+## Configuration
+
+### Destructive actions
+
+For actions that could have destructive effects on the user’s data (for example, *delete* or *remove*) we have a special style.
+This style can be applied by using `destructive` prop and it will work for both: primary and default variants. Take care when using the default variant to ensure that the red text has sufficient color contrast against the background where you are using it.
+
+### Icons
+
+Icon can be placed in front of a label to both clarify an action and call attention to a button.
+By passing an object in `icon` prop, there will be automatically included `` component.
+By default, the icon itself won't be customizable. Icons within buttons are hidden from assistive technology, so make sure your button has descriptive enough text, or is otherwise well-labelled (e.g. with `aria-label`).
+
+#### Icon-only
+
+There are certain instances where an icon will suffice in place of a text label, but use these cautiously.
+Icon-only buttons should always use clear and obvious icon, ideally accompanied by a tooltip or title. Make sure your button is adequately labelled for assistive technologies, either by the tooltip or by an `aria-label`.
+
+##### Fully rounded
+
+In most cases, whenever we display icon-only buttons (standalone, not in a *buttons group*) we tend to make button fully rounded - use `rounded` prop for that.
+Other than that please avoid customizing border-radius on buttons.
+
+### Tooltip
+
+By passing a string or node in `tooltip` prop, button will get a custom tooltip (visually different than a classic `title`) on `:hover` and `:focus`. The tooltip makes up part of the "accessible name" used by assistive technologies like screen readers. If you _do not_ wish the tooltip to be part of the accessible name, consider wrapping it in a ``. Tooltips can be temporarily dismissed on `Escape` key press.
+
+## Design
+
+
diff --git a/app/javascript/crayons/Buttons/__stories__/Buttons.stories.jsx b/app/javascript/crayons/Buttons/__stories__/Buttons.stories.jsx
new file mode 100644
index 000000000..ea64f34c9
--- /dev/null
+++ b/app/javascript/crayons/Buttons/__stories__/Buttons.stories.jsx
@@ -0,0 +1,76 @@
+import { h } from 'preact';
+import ButtonsDoc from './Buttons.mdx';
+import { ButtonNew as Button } from '@crayons';
+import CogIcon from '@images/cog.svg';
+
+export default {
+ component: Button,
+ title: 'BETA/Buttons',
+ parameters: {
+ docs: {
+ page: ButtonsDoc,
+ },
+ },
+ argTypes: {
+ primary: {
+ description:
+ 'This prop defines whether or not your button will have *primary* style (in practice: high contrast, filled with accent color). Keep in mind ideally there should be only one primary button per entire component, or sometimes even per entire view.',
+ table: {
+ defaultValue: { summary: false },
+ },
+ },
+ rounded: {
+ description:
+ 'By enabling this setting you can turn all corners of the button to be fully rounded. We usually use it **only** for buttons containing **icon without label**.',
+ table: {
+ defaultValue: { summary: false },
+ },
+ },
+ destructive: {
+ description:
+ 'For various destructive actions we can have special styling for button which will add red-ish coloring.',
+ table: {
+ defaultValue: { summary: false },
+ },
+ },
+ tooltip: {
+ description:
+ "If defined, button will have a custom tooltip on `:hover` and `:focus`. The tooltip content will form part of the button's accessible name unless passed as a `` with `aria-hidden='true'`",
+ control: {
+ type: 'text',
+ },
+ table: {
+ defaultValue: { summary: undefined },
+ },
+ },
+ icon: {
+ control: false,
+ description:
+ 'Even though the generated JSX code in this component will have an empty function as prop value (`