aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/molecules/layout
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/molecules/layout')
-rw-r--r--src/components/molecules/layout/code.module.scss307
-rw-r--r--src/components/molecules/layout/code.stories.tsx118
-rw-r--r--src/components/molecules/layout/code.test.tsx16
-rw-r--r--src/components/molecules/layout/code.tsx101
4 files changed, 542 insertions, 0 deletions
diff --git a/src/components/molecules/layout/code.module.scss b/src/components/molecules/layout/code.module.scss
new file mode 100644
index 0000000..19d1d70
--- /dev/null
+++ b/src/components/molecules/layout/code.module.scss
@@ -0,0 +1,307 @@
+@use "@styles/abstracts/functions" as fun;
+@use "@styles/abstracts/mixins" as mix;
+
+.wrapper {
+ :global {
+ .code-toolbar {
+ --gutter-size: clamp(#{fun.convert-px(75)}, 20vw, #{fun.convert-px(90)});
+ --toolbar-height: #{fun.convert-px(90)};
+
+ position: relative;
+ margin-top: calc(var(--toolbar-height) + var(--spacing-md));
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ --toolbar-height: #{fun.convert-px(60)};
+ }
+ }
+
+ .toolbar {
+ display: grid;
+ grid-template-columns: max-content minmax(0, 1fr);
+ justify-items: end;
+ width: 100%;
+ height: var(--toolbar-height);
+ position: absolute;
+ top: calc(var(--toolbar-height) * -1);
+ left: 0;
+ right: 0;
+ background: var(--color-bg-tertiary);
+ border: fun.convert-px(1) solid var(--color-border);
+
+ @include mix.media("screen") {
+ @include mix.dimensions("2xs") {
+ display: flex;
+ flex-flow: row wrap;
+ }
+ }
+ }
+
+ .toolbar-item {
+ display: flex;
+ align-items: center;
+ }
+
+ .toolbar-item:nth-child(1) {
+ grid-column: 1;
+ grid-row: 1 / 3;
+ margin-right: auto;
+ padding: 0 var(--spacing-sm);
+ background: var(--color-bg-code);
+ border-right: fun.convert-px(1) solid var(--color-border);
+ color: var(--color-primary-darker);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ }
+
+ .toolbar-item:nth-child(2) {
+ grid-column: 2;
+ grid-row: 1;
+ margin: 0 var(--spacing-2xs);
+ }
+
+ .toolbar-item:nth-child(3) {
+ grid-column: 2;
+ grid-row: 2;
+ margin: 0 var(--spacing-2xs);
+ }
+ }
+
+ pre[class*="language-"] {
+ max-height: max(30vw, fun.convert-px(300));
+ margin: var(--spacing-md) 0;
+ padding: 0;
+ position: relative;
+ background: var(--color-bg-secondary);
+ color: var(--color-fg);
+ border: fun.convert-px(1) solid var(--color-border);
+
+ > code {
+ display: block;
+ padding: var(--spacing-xs) 0 var(--spacing-xs)
+ calc(var(--gutter-size) + var(--spacing-xs));
+ }
+
+ .line-numbers-rows,
+ .command-line-prompt {
+ width: var(--gutter-size);
+ min-height: 100%;
+ padding: var(--spacing-xs) var(--spacing-2xs);
+ position: absolute;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ user-select: none;
+ background: var(--color-bg);
+ border-right: fun.convert-px(1) solid var(--color-border);
+ }
+
+ .token {
+ &.comment,
+ &.doc-comment {
+ color: var(--color-fg-light);
+ }
+
+ &.punctuation {
+ color: var(--color-fg);
+ }
+
+ &.attr-name,
+ &.hexcode,
+ &.inserted,
+ &.string {
+ color: var(--color-token-green);
+ }
+
+ &.class,
+ &.coord,
+ &.id,
+ &.function {
+ color: var(--color-token-purple);
+ }
+
+ &.builtin,
+ &.builtin.class-name,
+ &.property-access,
+ &.regex,
+ &.scope {
+ color: var(--color-token-magenta);
+ }
+
+ &.class-name,
+ &.constant,
+ &.global,
+ &.interpolation,
+ &.key,
+ &.package,
+ &.this,
+ &.title,
+ &.variable {
+ color: var(--color-token-blue);
+ }
+
+ &.combinator,
+ &.keyword,
+ &.operator,
+ &.pseudo-class,
+ &.pseudo-element,
+ &.rule,
+ &.selector,
+ &.unit {
+ color: var(--color-token-orange);
+ }
+
+ &.attr-value,
+ &.boolean,
+ &.number {
+ color: var(--color-token-yellow);
+ }
+
+ &.delimiter,
+ &.doctype,
+ &.parameter,
+ &.parent,
+ &.property,
+ &.shebang,
+ &.tag {
+ color: var(--color-token-cyan);
+ }
+
+ &.deleted {
+ color: var(--color-token-red);
+ }
+
+ &.punctuation.brace-hover,
+ &.punctuation.brace-selected {
+ background: var(--color-bg);
+ outline: solid fun.convert-px(1) var(--color-primary-light);
+ }
+ }
+
+ span.inline-color-wrapper {
+ background: url(fun.encode-svg(
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="gray" d="M0 0h2v2H0z"/><path fill="white" d="M0 0h1v1H0zM1 1h1v1H1z"/></svg>'
+ ));
+
+ /* Prevent glitches where 1px from the repeating pattern could be seen. */
+ background-position: center;
+ background-size: 110%;
+
+ display: inline-block;
+ height: 1.1ch;
+ width: 1.1ch;
+ margin: 0 0.5ch 0 0;
+ border: fun.convert-px(1) solid var(--color-bg);
+ outline: fun.convert-px(1) solid var(--color-border-dark);
+ overflow: hidden;
+ }
+
+ span.inline-color {
+ display: block;
+
+ /* To prevent visual glitches again */
+ height: 120%;
+ width: 120%;
+ }
+ }
+
+ pre.line-numbers {
+ counter-reset: lineNumber;
+
+ .line-numbers-rows {
+ > span {
+ counter-increment: lineNumber;
+
+ &::before {
+ display: block;
+ padding: 0 var(--spacing-xs);
+ content: counter(lineNumber);
+ color: var(--color-primary-darker);
+ text-align: right;
+ line-height: var(--line-height);
+ }
+ }
+ }
+ }
+
+ pre.command-line {
+ --gutter-size: clamp(
+ #{fun.convert-px(195)},
+ 48vw,
+ #{fun.convert-px(235)}
+ );
+
+ ~ .toolbar {
+ --gutter-size: clamp(
+ #{fun.convert-px(195)},
+ 48vw,
+ #{fun.convert-px(235)}
+ );
+ }
+
+ .command-line-prompt {
+ > span {
+ &::before {
+ display: block;
+ content: "";
+ }
+
+ &[data-user]::before {
+ content: "[" attr(data-user) "@" attr(data-host) "] $";
+ }
+
+ &[data-user="root"]::before {
+ content: "[" attr(data-user) "@" attr(data-host) "] #";
+ }
+
+ &[data-prompt]::before {
+ content: attr(data-prompt);
+ }
+ }
+ }
+ }
+
+ .copy-to-clipboard-button,
+ .prism-color-scheme-button {
+ display: block;
+ padding: fun.convert-px(3) var(--spacing-xs);
+ background: var(--color-bg);
+ border: 0.4ex solid var(--color-primary);
+ border-radius: fun.convert-px(30);
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2) fun.convert-px(-2)
+ var(--color-shadow),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5) fun.convert-px(-4)
+ var(--color-shadow);
+ color: var(--color-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 600;
+ transition: all 0.35s ease-in-out 0s;
+
+ &:hover,
+ &:focus {
+ transform: translateX(#{fun.convert-px(-2)})
+ translateY(#{fun.convert-px(-2)});
+ box-shadow: fun.convert-px(1) fun.convert-px(1) fun.convert-px(1)
+ var(--color-shadow-light),
+ fun.convert-px(1) fun.convert-px(2) fun.convert-px(2)
+ fun.convert-px(-2) var(--color-shadow-light),
+ fun.convert-px(3) fun.convert-px(4) fun.convert-px(5)
+ fun.convert-px(-4) var(--color-shadow-light),
+ fun.convert-px(4) fun.convert-px(7) fun.convert-px(8)
+ fun.convert-px(-3) var(--color-shadow-light);
+ }
+
+ &:focus {
+ text-decoration: underline var(--color-primary) fun.convert-px(3);
+ }
+
+ &:active {
+ text-decoration: none;
+ transform: translateY(#{fun.convert-px(2)});
+ box-shadow: 0 0 0 0 var(--color-shadow);
+ }
+ }
+ }
+}
diff --git a/src/components/molecules/layout/code.stories.tsx b/src/components/molecules/layout/code.stories.tsx
new file mode 100644
index 0000000..a2a6b2c
--- /dev/null
+++ b/src/components/molecules/layout/code.stories.tsx
@@ -0,0 +1,118 @@
+import { ComponentMeta, ComponentStory } from '@storybook/react';
+import { IntlProvider } from 'react-intl';
+import CodeComponent from './code';
+
+/**
+ * Code - Storybook Meta
+ */
+export default {
+ title: 'Molecules/Layout/Code',
+ component: CodeComponent,
+ args: {
+ filterOutput: false,
+ outputPattern: '#output#',
+ },
+ argTypes: {
+ children: {
+ control: {
+ type: 'text',
+ },
+ description: 'The code sample.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ filterOutput: {
+ control: {
+ type: 'boolean',
+ },
+ description: 'Filter the command line output.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: false },
+ },
+ type: {
+ name: 'boolean',
+ required: false,
+ },
+ },
+ language: {
+ control: {
+ type: 'text',
+ },
+ description: 'The code sample language.',
+ type: {
+ name: 'string',
+ required: true,
+ },
+ },
+ plugins: {
+ description: 'An array of Prism plugins to activate.',
+ type: {
+ name: 'object',
+ required: false,
+ value: {},
+ },
+ },
+ outputPattern: {
+ control: {
+ type: 'text',
+ },
+ description: 'The command line output pattern.',
+ table: {
+ category: 'Options',
+ defaultValue: { summary: '#output#' },
+ },
+ type: {
+ name: 'string',
+ required: false,
+ },
+ },
+ },
+ decorators: [
+ (Story) => (
+ <IntlProvider locale="en">
+ <Story />
+ </IntlProvider>
+ ),
+ ],
+} as ComponentMeta<typeof CodeComponent>;
+
+const Template: ComponentStory<typeof CodeComponent> = (args) => (
+ <CodeComponent {...args} />
+);
+
+const javascriptCodeSample = `
+const foo = () => {
+ return 'bar';
+}
+`;
+
+/**
+ * Code Stories - Code sample
+ */
+export const CodeSample = Template.bind({});
+CodeSample.args = {
+ children: javascriptCodeSample,
+ language: 'javascript',
+ plugins: ['line-numbers'],
+};
+
+const commandLineCode = `
+ls -lah
+#output#drwxr-x---+ 42 armand armand 4,0K 17 avril 11:15 .
+#output#drwxr-xr-x 4 root root 4,0K 30 mai 2021 ..
+#output#-rw-r--r-- 1 armand armand 2,0K 21 juil. 2021 .xinitrc
+`;
+
+/**
+ * Code Stories - Command Line
+ */
+export const CommandLine = Template.bind({});
+CommandLine.args = {
+ children: commandLineCode,
+ filterOutput: true,
+ language: 'bash',
+ plugins: ['command-line'],
+};
diff --git a/src/components/molecules/layout/code.test.tsx b/src/components/molecules/layout/code.test.tsx
new file mode 100644
index 0000000..ebcfae5
--- /dev/null
+++ b/src/components/molecules/layout/code.test.tsx
@@ -0,0 +1,16 @@
+import { render } from '@test-utils';
+import Code from './code';
+
+const code = `
+function foo() {
+ return 'bar';
+}
+`;
+
+const language = 'javascript';
+
+describe('Code', () => {
+ it('renders a code block', () => {
+ render(<Code language={language}>{code}</Code>);
+ });
+});
diff --git a/src/components/molecules/layout/code.tsx b/src/components/molecules/layout/code.tsx
new file mode 100644
index 0000000..2959ae5
--- /dev/null
+++ b/src/components/molecules/layout/code.tsx
@@ -0,0 +1,101 @@
+import usePrismPlugins, {
+ type PrismPlugin,
+} from '@utils/hooks/use-prism-plugins';
+import { FC } from 'react';
+import styles from './code.module.scss';
+
+export type PrismLanguage =
+ | 'apacheconf'
+ | 'bash'
+ | 'css'
+ | 'diff'
+ | 'docker'
+ | 'editorconfig'
+ | 'ejs'
+ | 'git'
+ | 'graphql'
+ | 'html'
+ | 'ignore'
+ | 'ini'
+ | 'javascript'
+ | 'jsdoc'
+ | 'json'
+ | 'jsx'
+ | 'makefile'
+ | 'markup'
+ | 'php'
+ | 'phpdoc'
+ | 'regex'
+ | 'scss'
+ | 'shell-session'
+ | 'smarty'
+ | 'tcl'
+ | 'toml'
+ | 'tsx'
+ | 'twig'
+ | 'yaml';
+
+export type OptionalPrismPlugin = Extract<
+ PrismPlugin,
+ | 'command-line'
+ | 'diff-highlight'
+ | 'inline-color'
+ | 'line-highlight'
+ | 'line-numbers'
+>;
+
+export type CodeProps = {
+ /**
+ * The code to highlight.
+ */
+ children: string;
+ /**
+ * Filter command line output. Default: false.
+ */
+ filterOutput?: boolean;
+ /**
+ * The code language.
+ */
+ language: PrismLanguage;
+ /**
+ * The optional Prism plugins.
+ */
+ plugins?: OptionalPrismPlugin[];
+ /**
+ * Filter command line output using the given string. Default: #output#
+ */
+ outputPattern?: string;
+};
+
+/**
+ * Code component
+ *
+ * Render a code block with syntax highlighting.
+ */
+const Code: FC<CodeProps> = ({
+ children,
+ filterOutput = false,
+ language,
+ plugins = [],
+ outputPattern = '#output#',
+}) => {
+ const { pluginsAttribute, pluginsClassName } = usePrismPlugins(plugins);
+
+ const outputAttribute = filterOutput
+ ? { 'data-filter-output': outputPattern }
+ : {};
+
+ return (
+ <div className={styles.wrapper}>
+ <pre
+ className={`language-${language} ${pluginsClassName}`}
+ {...pluginsAttribute}
+ {...outputAttribute}
+ >
+ <code className={`language-${language}`}>{children}</code>
+ </pre>
+ </div>
+ );
+};
+
+export default Code;