diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 505586807829825cbf77c95e35f4af643f297f8d..d51dd8c6c900093fb7cccbfa52c1b879277ce1ff 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -28,34 +28,27 @@ <codeStyleSettings language="HTML"> <option name="SOFT_MARGINS" value="100" /> <indentOptions> - <option name="INDENT_SIZE" value="2" /> - <option name="CONTINUATION_INDENT_SIZE" value="2" /> - <option name="TAB_SIZE" value="2" /> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="USE_TAB_CHARACTER" value="true" /> </indentOptions> </codeStyleSettings> <codeStyleSettings language="JavaScript"> <option name="SOFT_MARGINS" value="100" /> <indentOptions> - <option name="INDENT_SIZE" value="2" /> - <option name="CONTINUATION_INDENT_SIZE" value="2" /> - <option name="TAB_SIZE" value="2" /> <option name="USE_TAB_CHARACTER" value="true" /> </indentOptions> </codeStyleSettings> <codeStyleSettings language="TypeScript"> <option name="SOFT_MARGINS" value="100" /> <indentOptions> - <option name="INDENT_SIZE" value="2" /> - <option name="CONTINUATION_INDENT_SIZE" value="2" /> - <option name="TAB_SIZE" value="2" /> <option name="USE_TAB_CHARACTER" value="true" /> </indentOptions> </codeStyleSettings> <codeStyleSettings language="Vue"> <option name="SOFT_MARGINS" value="100" /> <indentOptions> - <option name="CONTINUATION_INDENT_SIZE" value="2" /> + <option name="INDENT_SIZE" value="4" /> + <option name="TAB_SIZE" value="4" /> <option name="USE_TAB_CHARACTER" value="true" /> </indentOptions> </codeStyleSettings> diff --git a/.prettierrc b/.prettierrc index 3f7802c3728f1ae67e245f68735362eb8fa4fe4b..a3896e01760b9b7bb8f1c4f614496e8875c910a9 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,8 +1,11 @@ { "useTabs": true, + "tabWidth": 4, "singleQuote": true, "trailingComma": "none", "printWidth": 100, + "bracketSameLine": true, + "plugins": ["prettier-plugin-svelte"], "overrides": [ { diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 6455613828f9fb19cf5bc3cf9684392c495e0e24..dbd1c7ac230a22cec028535e7fc084e94c432e6d 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -8,7 +8,7 @@ const preview: Preview = { date: /Date$/i } } - }, + } }; export default preview; diff --git a/package.json b/package.json index b5728df8008f6f7f97aa126b305ffd5eac9be306..e70aa0acd82e7394f92c06fd92ba28b56f6f80e2 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,12 @@ "!dist/**/*.test.*", "!dist/**/*.spec.*", "!dist/**/*.stories.*", + "!dist/**/*.internal.*", "src/lib", "!src/lib/**/*.test.*", "!src/lib/**/*.spec.*", - "!src/lib/**/*.stories.*" + "!src/lib/**/*.stories.*", + "!src/lib/**/*.internal.*" ], "sideEffects": [ "**/*.css" @@ -40,6 +42,9 @@ }, "./barbi.css": { "default": "./dist/barbi.css" + }, + "./icons": { + "default": "./dist/icons.js" } }, "peerDependencies": { diff --git a/src/lib/atoms/Button.stories.svelte b/src/lib/atoms/Button.stories.svelte index 9f2fcd921ad0683863a1d1af8a4b93ff7d1917f6..6ca5f3a430ddacbca38124f166490536558c23b2 100644 --- a/src/lib/atoms/Button.stories.svelte +++ b/src/lib/atoms/Button.stories.svelte @@ -4,7 +4,7 @@ const { Story } = defineMeta({ title: 'Components/Atoms/Button', - component: Button, + component: Button }); </script> @@ -45,17 +45,20 @@ <Story name="Invisible Button"> {#snippet children(args)} <BarbiTheme /> - <p>There is a button on this page, but you can't see it. Use the `placeholder` prop to create a button that occupies only vertical space, - with no interaction available. Useful for reserving space.</p> + <p> + There is a button on this page, but you can't see it. Use the `placeholder` prop to + create a button that occupies only vertical space, with no interaction available. Useful + for reserving space. + </p> <Button placeholder {...args}>You aren't able to see this button at all!</Button> {/snippet} </Story> <style> .storyboard-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - grid-auto-rows: max-content; - gap: var(--spacing-md); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-auto-rows: max-content; + gap: var(--spacing-md); } </style> diff --git a/src/lib/atoms/Button.svelte b/src/lib/atoms/Button.svelte index c63b7c2706c8add626730bdee228c912006e9670..8fccc59219b65579617d3fad38d82fc5a59da8c5 100644 --- a/src/lib/atoms/Button.svelte +++ b/src/lib/atoms/Button.svelte @@ -1,79 +1,91 @@ -<script lang="ts"> - import type { HTMLButtonAttributes } from 'svelte/elements' - import Panel from './Panel.svelte'; - - export type Props = { - children?: HTMLButtonAttributes['children'], - variant?: 'primary' | 'secondary' | 'success' | 'error' - } +<script module lang="ts"> + import type { Props as PanelProps } from './Panel.svelte'; + import type { Snippet } from 'svelte'; - const { children, variant, placeholder, ...props }: Props = $props() - const tag = $derived(props.href != null ? 'a' : 'button') + export type Props = Omit<PanelProps, 'variant'> & { + variant?: 'primary' | 'secondary' | 'success' | 'error'; + children?: Snippet; + href?: string; + }; +</script> +<script lang="ts"> + import Panel from './Panel.svelte'; + const { children, variant, placeholder, ...props }: Props = $props(); + const tag = $derived(props.href != null ? 'a' : 'button'); </script> -<Panel class={['barbi-button', `barbi-button-${variant}`, !!placeholder && 'placeholder']} space="sm" {...props} tag={tag} hover active onclick={() => alert("foo")}> +<Panel + class={['barbi-button', `barbi-button-${variant}`, !!placeholder && 'placeholder']} + space="sm" + {...props} + {tag} + hover + active +> {@render children?.()} </Panel> <style> :global { - a.panel.barbi-button, - .panel.barbi-button a { - text-decoration: none; - } - .panel.barbi-button { - cursor: pointer; - --component-background: var(--colour-primary); - } - - .panel.barbi-button.placeholder { - cursor: inherit; - pointer-events: none !important; - width: 0 !important; - opacity: 0 !important; - } + a.panel.barbi-button, + .panel.barbi-button a { + text-decoration: none; + } + .panel.barbi-button { + cursor: pointer; + display: inline-flex; + flex-direction: row; + align-items: center; + --component-background: var(--colour-primary); + } - .panel.barbi-button:disabled { - cursor: not-allowed; - --component-background: var(--colour-concrete); - --component-colour: var(--colour-slate); - --border-colour: var(--colour-steel); - --colour-shadow: var(--colour-steel); - } + .panel.barbi-button.placeholder { + cursor: inherit; + pointer-events: none !important; + width: 0 !important; + opacity: 0 !important; + } - .panel.barbi-button:hover:not(:disabled) { - cursor: pointer; - --component-background: var(--colour-secondary); - } + .panel.barbi-button:disabled { + cursor: not-allowed; + --component-background: var(--colour-concrete); + --component-colour: var(--colour-slate); + --border-colour: var(--colour-steel); + --colour-shadow: var(--colour-steel); + } - .panel.barbi-button.barbi-button-primary:not(:disabled) { - --component-background: var(--colour-primary); - } - .panel.barbi-button.barbi-button-primary:hover:not(:disabled) { - --component-background: var(--colour-secondary); - } + .panel.barbi-button:hover:not(:disabled) { + cursor: pointer; + --component-background: var(--colour-secondary); + } - .panel.barbi-button.barbi-button-secondary:not(:disabled) { - --component-background: var(--colour-emphasis); - } - .panel.barbi-button.barbi-button-secondary:hover:not(:disabled) { - --component-background: var(--colour-mustard); - } + .panel.barbi-button.barbi-button-primary:not(:disabled) { + --component-background: var(--colour-primary); + } + .panel.barbi-button.barbi-button-primary:hover:not(:disabled) { + --component-background: var(--colour-secondary); + } - .panel.barbi-button.barbi-button-success:not(:disabled) { - --component-background: var(--colour-success); - } - .panel.barbi-button.barbi-button-success:hover:not(:disabled) { - --component-background: var(--colour-pastel); - } + .panel.barbi-button.barbi-button-secondary:not(:disabled) { + --component-background: var(--colour-emphasis); + } + .panel.barbi-button.barbi-button-secondary:hover:not(:disabled) { + --component-background: var(--colour-mustard); + } - .panel.barbi-button.barbi-button-error:not(:disabled) { - --component-background: var(--colour-error); - } - .panel.barbi-button.barbi-button-error:hover:not(:disabled) { - --component-background: var(--colour-mustard); - } + .panel.barbi-button.barbi-button-success:not(:disabled) { + --component-background: var(--colour-success); + } + .panel.barbi-button.barbi-button-success:hover:not(:disabled) { + --component-background: var(--colour-pastel); + } + .panel.barbi-button.barbi-button-error:not(:disabled) { + --component-background: var(--colour-error); + } + .panel.barbi-button.barbi-button-error:hover:not(:disabled) { + --component-background: var(--colour-mustard); + } } -</style> \ No newline at end of file +</style> diff --git a/src/lib/atoms/Panel.stories.svelte b/src/lib/atoms/Panel.stories.svelte index 6592f6d65788d5d942dfc08e63a525dd829c8aca..163d3a4f443f354272de8b5901b52b51a8ae7b82 100644 --- a/src/lib/atoms/Panel.stories.svelte +++ b/src/lib/atoms/Panel.stories.svelte @@ -4,7 +4,7 @@ const { Story } = defineMeta({ title: 'Components/Atoms/Panel', - component: Panel, + component: Panel }); </script> @@ -14,30 +14,18 @@ <Story name="Simple Panel"> {#snippet children(args)} <BarbiTheme /> - <Panel {...args}> - The most basic panel type - </Panel> - <Panel flat {...args}> - A basic panel without shadow - </Panel> + <Panel {...args}>The most basic panel type</Panel> + <Panel flat {...args}>A basic panel without shadow</Panel> {/snippet} </Story> <Story name="Sizable Panel"> {#snippet children(args)} <BarbiTheme /> - <Panel space="none" {...args}> - space="none" - </Panel> - <Panel space="sm" {...args}> - space="sm" - </Panel> - <Panel space="md" {...args}> - space="md" - </Panel> - <Panel space="lg" {...args}> - space="lg" - </Panel> + <Panel space="none" {...args}>space="none"</Panel> + <Panel space="sm" {...args}>space="sm"</Panel> + <Panel space="md" {...args}>space="md"</Panel> + <Panel space="lg" {...args}>space="lg"</Panel> {/snippet} </Story> @@ -45,36 +33,16 @@ {#snippet children(args)} <BarbiTheme /> <div class="storyboard-grid"> - <Panel variant="primary" {...args}> - variant="primary" - </Panel> - <Panel variant="secondary" {...args}> - variant="secondary" - </Panel> - <Panel variant="success" {...args}> - variant="success" - </Panel> - <Panel variant="error" {...args}> - variant="error" - </Panel> - <Panel variant="emphasis" {...args}> - variant="emphasis" - </Panel> - <Panel variant="mustard" {...args}> - variant="mustard" - </Panel> - <Panel variant="pastel" {...args}> - variant="pastel" - </Panel> - <Panel variant="brick" {...args}> - variant="brick" - </Panel> - <Panel variant="neon" {...args}> - variant="neon" - </Panel> - <Panel variant="slate" {...args}> - variant="slate" - </Panel> + <Panel variant="primary" {...args}>variant="primary"</Panel> + <Panel variant="secondary" {...args}>variant="secondary"</Panel> + <Panel variant="success" {...args}>variant="success"</Panel> + <Panel variant="error" {...args}>variant="error"</Panel> + <Panel variant="emphasis" {...args}>variant="emphasis"</Panel> + <Panel variant="mustard" {...args}>variant="mustard"</Panel> + <Panel variant="pastel" {...args}>variant="pastel"</Panel> + <Panel variant="brick" {...args}>variant="brick"</Panel> + <Panel variant="neon" {...args}>variant="neon"</Panel> + <Panel variant="slate" {...args}>variant="slate"</Panel> </div> {/snippet} </Story> @@ -92,20 +60,22 @@ {#snippet children(args)} <BarbiTheme /> <Panel {...args} hover active> - hover={true} active={true} + hover={true} active={true} (regular panel) </Panel> <Panel {...args} tag="button" hover active> - hover={true} active={true} + hover={true} active={true} (button tag panel) + </Panel> + <Panel {...args} tag="code" hover active> + hover={true} active={true} (code tag panel) </Panel> {/snippet} </Story> - <style> .storyboard-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - grid-auto-rows: max-content; - gap: var(--spacing-md); + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + grid-auto-rows: max-content; + gap: var(--spacing-md); } </style> diff --git a/src/lib/atoms/Panel.svelte b/src/lib/atoms/Panel.svelte index ec0e206af84ec945275d7359663190d861110e92..2483a0e762b1dc71cc3eff872dbfd89eecb2d448 100644 --- a/src/lib/atoms/Panel.svelte +++ b/src/lib/atoms/Panel.svelte @@ -28,8 +28,9 @@ flat = false, class: className, children = null, + shadowMargin = true, ...rest - }: Props<Tag> = $props(); + }: Props = $props(); </script> <svelte:element @@ -41,6 +42,7 @@ 'shadow-sm': shadow === 'sm', 'shadow-md': shadow === 'md', 'shadow-lg': shadow === 'lg', + 'shadow-margin': shadowMargin, hover, active, full, @@ -76,6 +78,11 @@ box-shadow: var(--shadow-md); } + .panel:not(.flat).shadow-margin { + margin-right: var(--spacing-sm); + margin-bottom: var(--spacing-sm); + } + .panel.primary { --component-background: var(--colour-primary); } diff --git a/src/lib/atoms/popover/Popover.stories.svelte b/src/lib/atoms/popover/Popover.stories.svelte new file mode 100644 index 0000000000000000000000000000000000000000..61b8615d4a1cf24536c9b2dc716e9c0deda291e9 --- /dev/null +++ b/src/lib/atoms/popover/Popover.stories.svelte @@ -0,0 +1,65 @@ +<script module> + import { Anchor, BarbiTheme, Popover } from '$lib'; + import { defineMeta } from '@storybook/addon-svelte-csf'; + + const { Story } = defineMeta({ + title: 'Components/Atoms/Popover', + component: Popover, + argTypes: { + position: { + name: 'Anchor Position', + control: 'radio', + options: [ + Anchor.TopLeft, + Anchor.TopCenter, + Anchor.TopRight, + Anchor.BottomLeft, + Anchor.BottomCenter, + Anchor.BottomRight + ] + } + }, + args: { + position: Anchor.BottomRight, + spaceHorizontal: '0rem', + spaceVertical: 'var(--spacing-sm)' + } + }); +</script> + +<script> + import { Panel } from '$lib'; +</script> + +<Story name="Simple Popover"> + {#snippet children(args)} + <BarbiTheme /> + <div class="storybook-center"> + <Popover {...args}> + <div class="content-wrapper">This is the trigger, click me</div> + + {#snippet popover()} + <Panel>This element contains a popover</Panel> + {/snippet} + </Popover> + </div> + {/snippet} +</Story> + +<style> + .storybook-center { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + } + + .content-wrapper { + border: 1px solid black; + padding: 2px 4px; + } + + :global(.sb-main-padded) { + padding: 0 !important; + } +</style> diff --git a/src/lib/atoms/popover/Popover.svelte b/src/lib/atoms/popover/Popover.svelte new file mode 100644 index 0000000000000000000000000000000000000000..156f8bb4bb04f25176d8bb14a646c41d08829ac9 --- /dev/null +++ b/src/lib/atoms/popover/Popover.svelte @@ -0,0 +1,126 @@ +<script module lang="ts"> + import type { Snippet } from 'svelte'; + import type { BarbiClass } from '../../utilTypes.js'; + import type { AnchorPoint } from './utils.ts'; + + export type Props = { + children?: Snippet; + popover?: Snippet; + wrapperClass?: BarbiClass; + class?: BarbiClass; + position?: AnchorPoint; + ontoggle?: () => void; + enabled?: boolean; + style?: string; + spaceVertical?: string; + spaceHorizontal?: string; + open?: boolean; + }; +</script> + +<script lang="ts"> + import { Anchor, calculatePosition } from './utils.ts'; + + let { + children, + wrapperClass, + class: className, + popover, + ontoggle, + enabled = true, + style, + position = Anchor.BottomLeft, + spaceVertical = 'var(--spacing-sm)', + spaceHorizontal = '0rem', + open = $bindable(false) + }: Props = $props(); + + let triggerElement = $state.raw(null); + let dialogElement = $state.raw(null); + // let open = $state(false); + + $effect(() => { + if (!dialogElement) return; + if (enabled && open) { + dialogElement.showModal(); + requestAnimationFrame(reposition); + } else { + dialogElement.close(); + } + if (enabled && ontoggle) { + ontoggle(open); + } + }); + + function openPopover() { + if (enabled) { + open = true; + } + } + + function onClickOut(event) { + if ( + open && + !dialogElement.contains(event.target) && + !triggerElement.contains(event.target) + ) { + open = false; + } + } + + function reposition() { + if (!triggerElement || !dialogElement) return; + calculatePosition(position, triggerElement, dialogElement); + } +</script> + +<svelte:window onclick={onClickOut} onresize={reposition} /> + +<div + class={['popover-wrapper', wrapperClass]} + style={`${style ? style : ''}; --component-gap-vertical: ${spaceVertical}; --component-gap-horizontal: ${spaceHorizontal};`} +> + <div bind:this={triggerElement} onclick={openPopover}> + {@render children?.()} + </div> + <dialog bind:this={dialogElement} class={[className, 'popover', { open }]}> + {@render popover?.()} + </dialog> +</div> + +<style> + .popover-wrapper { + --component-gap-vertical: var(--spacing-md); + --component-gap-horizontal: var(--spacing-md); + display: inline-block; + position: relative; + } + + .popover { + position: fixed; + border: none; + background-color: transparent; + z-index: 500; + outline: none; + } + + .popover.open { + animation: popopen var(--transition-medium) forwards; + } + + @keyframes popopen { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } + } + + .popover::backdrop { + display: none; + background-color: transparent; + } +</style> diff --git a/src/lib/atoms/popover/utils.ts b/src/lib/atoms/popover/utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..813f068a6e18c811a96b8d5cc7d9ec6050536010 --- /dev/null +++ b/src/lib/atoms/popover/utils.ts @@ -0,0 +1,94 @@ +export const Anchor = { + TopLeft: 'TopLeft', + TopCenter: 'TopCenter', + TopRight: 'TopRight', + BottomLeft: 'BottomLeft', + BottomCenter: 'BottomCenter', + BottomRight: 'BottomRight' +} as const; + +export type AnchorType = typeof Anchor; +export type AnchorName = keyof AnchorType; +export type AnchorPoint = AnchorType[AnchorName]; + +function offsetStyle(pixels: number, varType: string, sign: number): string { + const signValue = sign > 0 ? '+' : '-'; + return `calc(${pixels}px ${signValue} var(--component-gap-${varType}))`; +} + +export function calculatePosition( + anchor: AnchorPoint, + trigger: HTMLDivElement, + dialog: HTMLDivElement +) { + const triggerRect = trigger.getBoundingClientRect(); + const dialogRect = dialog.getBoundingClientRect(); + + let left: number; + let top: number; + let vertSign = 1; + let horizSign = 1; + + switch (anchor) { + case Anchor.TopLeft: + left = triggerRect.left; + top = triggerRect.top - dialogRect.height; + horizSign = 1; + vertSign = -1; + break; + case Anchor.TopCenter: + left = triggerRect.left + triggerRect.width / 2 - dialogRect.width / 2; + top = triggerRect.top - dialogRect.height; + horizSign = 0; + vertSign = -1; + break; + case Anchor.TopRight: + left = triggerRect.right - dialogRect.width; + top = triggerRect.top - dialogRect.height; + horizSign = -1; + vertSign = -1; + + break; + case Anchor.BottomLeft: + left = triggerRect.left; + top = triggerRect.bottom; + horizSign = 1; + vertSign = 1; + + break; + case Anchor.BottomCenter: + left = triggerRect.left + triggerRect.width / 2 - dialogRect.width / 2; + top = triggerRect.bottom; + horizSign = 0; + vertSign = 1; + break; + case Anchor.BottomRight: + left = triggerRect.right - dialogRect.width; + top = triggerRect.bottom; + horizSign = -1; + vertSign = 1; + break; + } + + // Adjust for window boundaries + if (left < 0) left = 0; + if (left + dialogRect.width > window.innerWidth) { + left = window.innerWidth - dialogRect.width; + } + if (top < 0) top = 0; + if (top + dialogRect.height > window.innerHeight) { + top = window.innerHeight - dialogRect.height; + } + + if (vertSign) { + dialog.style.top = offsetStyle(top, 'vertical', vertSign); + } else { + dialog.style.top = `${top}px`; + } + + if (horizSign) { + dialog.style.left = offsetStyle(left, 'horizontal', horizSign); + } else { + dialog.style.left = `${left}px`; + } +} diff --git a/src/lib/components/modal/CustomIcon.internal.svelte b/src/lib/components/modal/CustomIcon.internal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ebe5de82666d85b875c77398e03c7ad86cd60c90 --- /dev/null +++ b/src/lib/components/modal/CustomIcon.internal.svelte @@ -0,0 +1,8 @@ +<script> + const { class: className, ...props } = $props(); +</script> + +<svg viewBox="0 0 200 110" xmlns="http://www.w3.org/2000/svg" + class={['icon', className]} {...props}> + <path d="m1.636 5.368 196.73 1.5-97.764 97.764-98.964-99.264z" fill="var(--colour-icon)"/> +</svg> diff --git a/src/lib/components/modal/Dropdown.stories.svelte b/src/lib/components/modal/Dropdown.stories.svelte new file mode 100644 index 0000000000000000000000000000000000000000..81e21295459d625cc273fbcc83299d0250a78ba9 --- /dev/null +++ b/src/lib/components/modal/Dropdown.stories.svelte @@ -0,0 +1,139 @@ +<script module> + import { Anchor, BarbiTheme, Dropdown } from '$lib'; + import { defineMeta } from '@storybook/addon-svelte-csf'; + import CustomIcon from './CustomIcon.internal.svelte'; + + const { Story } = defineMeta({ + title: 'Components/Components/Dropdown', + component: Dropdown, + argTypes: { + alternate: { + name: 'Alternate Style', + control: 'boolean', + }, + flat: { + name: 'Flat Style', + control: 'boolean', + }, + options: { + table: { + disable: true + } + } + } + }); +</script> + +<Story + name="Simple Dropdown" + args={{ + label: "I'm a dropdown", + options: [ + { value: '1', label: 'I log to the console', onClick() { console.log("First button") } }, + { value: '2', label: "I'm an external link", href: 'https://example.com' }, + ] + }} +> + {#snippet children(args)} + <BarbiTheme /> + <div class="storybook-center"> + <Dropdown {...args} /> + </div> + {/snippet} +</Story> + +<Story + name="Alternate Colour Dropdown" + args={{ + label: "This dropdown uses variant='secondary'", + alternate: true, + options: [ + { value: '1', label: 'I log to the console', onClick() { console.log("First button") } }, + { value: '2', label: "I'm an external link", href: 'https://example.com' }, + ] + }} +> + {#snippet children(args)} + <BarbiTheme /> + <div class="storybook-center"> + <Dropdown {...args} /> + </div> + {/snippet} +</Story> + +<Story + name="Flat Style Dropdown" + args={{ + label: "I'm Flat!", + flat: true, + options: [ + { value: '1', label: 'I log to the console', onClick() { console.log("First button") } }, + { value: '2', label: "I'm an external link", href: 'https://example.com' }, + ] + }} +> + {#snippet children(args)} + <BarbiTheme /> + <div class="storybook-center"> + <Dropdown {...args} /> + </div> + {/snippet} +</Story> + +<Story + name="Only Icon" + args={{ + label: "", + options: [ + { value: '1', label: 'I log to the console', onClick() { console.log("First button") } }, + { value: '2', label: "I'm an external link", href: 'https://example.com' }, + ] + }} +> + {#snippet children(args)} + <BarbiTheme /> + <div class="storybook-center"> + <Dropdown {...args} /> + <span>Creating a dropdown with an empty label will only render the icon</span> + </div> + {/snippet} +</Story> + +<Story + name="With Custom Icon" + args={{ + label: "It won't spin", + icon: CustomIcon, + options: [ + { value: '1', label: 'I log to the console', onClick() { console.log("First button") } }, + { value: '2', label: "I'm an external link", href: 'https://example.com' }, + ] + }} +> + {#snippet children(args)} + <BarbiTheme /> + <div class="storybook-center"> + <Dropdown {...args} /> + <span>Pass a component to the 'icon' prop that uses the global '.icon' class, and merges it with a 'class' property</span> + </div> + {/snippet} +</Story> + +<style> + .storybook-center { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + } + + .storybook-center span { + max-width: 50%; + margin: 10px 0; + } + + :global(.sb-main-padded) { + padding: 0 !important; + } +</style> diff --git a/src/lib/components/modal/Dropdown.svelte b/src/lib/components/modal/Dropdown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6ab9d745de6f4508846929b0b6601f04071784ca --- /dev/null +++ b/src/lib/components/modal/Dropdown.svelte @@ -0,0 +1,92 @@ +<script lang="ts" module> + import type { Component } from 'svelte'; + + export type Props = { + label?: string; + options?: DropdownOption[]; + alternate?: boolean, + flat?: boolean, + icon?: Component<any> + }; + + export type DropdownOption = { + label: string; + value: string; + onClick?: () => void; + href?: () => void; + }; +</script> + +<script lang="ts"> + import Button from '../../atoms/Button.svelte'; + import Panel from '../../atoms/Panel.svelte'; + import Popover from '../../atoms/popover/Popover.svelte'; + import AngleDownIcon from '../..//patterns/icons/AngleDownIcon.svelte'; + + const { label = '', options = [], alternate = false, flat = false, icon: Icon = AngleDownIcon }: Props = $props(); + let open = $state(false); + let shouldAnimate = $derived.by(() => Icon === AngleDownIcon) + + function wrapOnClick(option: DropdownOption) { + if (option.onClick) { + const onClick = option.onClick; + return (...args: any[]) => { + open = false; + return onClick(...args); + }; + } else if (option.href) { + return () => { + open = false; + }; + } + } +</script> + +<Popover bind:open> + <Button variant={alternate ? 'secondary' : 'primary'} flat={flat}> + <span class="dropdown-label">{label}</span> + <Icon style="--size-icon: 1.25rem;" class={['dropdown-arrow', shouldAnimate && 'dropdown-arrow-animate', { open }]} /> + </Button> + + {#snippet popover()} + <Panel space="none" class="dropdown-popover-list" flat={flat}> + {#each options as option} + <Button variant={alternate ? 'success' : 'secondary'} flat onclick={wrapOnClick(option)} href={option.href}> + {option.label} + </Button> + {/each} + </Panel> + {/snippet} +</Popover> + +<style> + :global { + .dropdown-arrow { + margin-left: var(--spacing-md); + transition: all var(--transition-fast); + transform: rotate3d(0, 0, 1, 0deg); + } + + .dropdown-arrow.dropdown-arrow-animate.open { + transform: rotate3d(0, 0, 1, 180deg); + } + + .panel.dropdown-popover-list { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: end; + } + .panel.dropdown-popover-list .barbi-button { + border-width: 0; + border-bottom-width: var(--border-width); + } + .panel.dropdown-popover-list .barbi-button:last-child { + border-width: 0; + } + } + + .dropdown-label:empty + :global(.dropdown-arrow) { + margin-left: 0; + } +</style> diff --git a/src/lib/icons.ts b/src/lib/icons.ts new file mode 100644 index 0000000000000000000000000000000000000000..272d1aaa97128789dd51945ac718310a9b782279 --- /dev/null +++ b/src/lib/icons.ts @@ -0,0 +1,3 @@ +import ArrowDownIcon from './patterns/icons/AngleDownIcon.svelte'; + +export { ArrowDownIcon }; diff --git a/src/lib/index.ts b/src/lib/index.ts index 3ffd12aac5dc7d9c3967891a5d48a1cddd7705eb..49856ce060898e7e586db600e46d9483d114085f 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -2,5 +2,22 @@ import BarbiTheme from './theme/BarbiTheme.svelte'; import Panel from './atoms/Panel.svelte'; import Button from './atoms/Button.svelte'; import CheckerboardPattern from './patterns/CheckerboardPattern.svelte'; +import Popover from './atoms/popover/Popover.svelte'; +import { Anchor } from './atoms/popover/utils.js'; +import Dropdown from './components/modal/Dropdown.svelte'; -export { BarbiTheme, CheckerboardPattern, Panel, Button }; +import type { AnchorName } from './atoms/popover/utils.ts'; + +// Utilities +export { BarbiTheme, CheckerboardPattern, Anchor }; + +// Atoms +export { + Panel, Button, Popover, +} + +// Components +export { Dropdown }; + +// Types +export type { AnchorName }; diff --git a/src/lib/patterns/CheckerboardPattern.stories.svelte b/src/lib/patterns/CheckerboardPattern.stories.svelte index e2ef5a266352dbd835c36cb5d65bf28f20d6e2b3..937d657dd5510fa7e6d533e1c38945a4bca80cc7 100644 --- a/src/lib/patterns/CheckerboardPattern.stories.svelte +++ b/src/lib/patterns/CheckerboardPattern.stories.svelte @@ -8,10 +8,14 @@ // tags: ['autodocs'], argTypes: { scale: { name: 'Scale', control: 'number' }, - scrollDirection: { name: 'Scroll Direction', control: 'radio', options: [null, 'up-left'] }, + scrollDirection: { + name: 'Scroll Direction', + control: 'radio', + options: [null, 'up-left'] + } }, args: { - scale: 1, + scale: 1 } }); </script> @@ -36,20 +40,14 @@ {/snippet} </Story> -<Story - name="Scaled Pattern" - args={{ scale: 4 }} -> +<Story name="Scaled Pattern" args={{ scale: 4 }}> {#snippet children(args)} <BarbiTheme /> <CheckerboardPattern {...args} /> {/snippet} </Story> -<Story - name="Scrolling Pattern" - args={{ scale: 4, scrollDirection: 'up-left' }} -> +<Story name="Scrolling Pattern" args={{ scale: 4, scrollDirection: 'up-left' }}> {#snippet children(args)} <BarbiTheme /> <CheckerboardPattern {...args} /> diff --git a/src/lib/patterns/CheckerboardPattern.svelte b/src/lib/patterns/CheckerboardPattern.svelte index 9269a47a64e2d810211e3518c21c0f507732e7cc..6fb0bfdae6c170f515d6752a1fc077afd446324a 100644 --- a/src/lib/patterns/CheckerboardPattern.svelte +++ b/src/lib/patterns/CheckerboardPattern.svelte @@ -1,13 +1,18 @@ <script> - const { scale = 1, scrollDirection = undefined, class: className = undefined, ...props } = $props(); + const { + scale = 1, + scrollDirection = undefined, + class: className = undefined, + ...props + } = $props(); - let height = $state(0) - let width = $state(0) + let height = $state(0); + let width = $state(0); </script> <svg xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 {width/scale} {height/scale}" + viewBox="0 0 {width / scale} {height / scale}" width="100%" height="100%" class={className} @@ -16,7 +21,13 @@ {...props} > <defs> - <pattern id="checks" patternUnits="userSpaceOnUse" width="16" height="16" class={scrollDirection ? ['scroll', scrollDirection] : []}> + <pattern + id="checks" + patternUnits="userSpaceOnUse" + width="16" + height="16" + class={scrollDirection ? ['scroll', scrollDirection] : []} + > <rect width="16" height="16" fill="var(--pattern-light)" /> <rect width="8" height="8" x="0" y="0" fill="var(--pattern-dark)" /> <rect width="8" height="8" x="8" y="8" fill="var(--pattern-dark)" /> @@ -26,20 +37,20 @@ </svg> <style> - .scroll { - animation: scroll-up-left 2s linear infinite; - } + .scroll { + animation: scroll-up-left 2s linear infinite; + } - .scroll.up-left { - animation-name: scroll-up-left; - } + .scroll.up-left { + animation-name: scroll-up-left; + } - @keyframes scroll-up-left { - 0% { - transform: translate3d(0px, 0px, 0px); - } - 100% { - transform: translate3d(-16px, -16px, 0px); - } - } -</style> \ No newline at end of file + @keyframes scroll-up-left { + 0% { + transform: translate3d(0px, 0px, 0px); + } + 100% { + transform: translate3d(-16px, -16px, 0px); + } + } +</style> diff --git a/src/lib/patterns/icons/AngleDownIcon.svelte b/src/lib/patterns/icons/AngleDownIcon.svelte new file mode 100644 index 0000000000000000000000000000000000000000..acd641496db2b7afab6c8335bd78b42d267a0741 --- /dev/null +++ b/src/lib/patterns/icons/AngleDownIcon.svelte @@ -0,0 +1,15 @@ +<script> + const { class: className, ...props } = $props(); +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + class={['icon', className]} + viewBox="0 0 448 512" + {...props} +> + <path + style="fill: var(--colour-icon)" + d="M201.4 374.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 306.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z" + /> +</svg> diff --git a/src/lib/theme/BarbiTheme.svelte b/src/lib/theme/BarbiTheme.svelte index 110b78aeb160130e61141ce7b4986b87090a44d4..88e330e54e7793e80bfa58fa3632ad5d84fb8a18 100644 --- a/src/lib/theme/BarbiTheme.svelte +++ b/src/lib/theme/BarbiTheme.svelte @@ -64,10 +64,10 @@ } button { - font-family: var(--font-family-body); - font-size: var(--font-size-base); - color: var(--colour-text); - line-height: 1.25; + font-family: var(--font-family-body); + font-size: var(--font-size-base); + color: var(--colour-text); + line-height: 1.25; } /* Set core body defaults */ @@ -108,6 +108,8 @@ --colour-text: var(--colour-contrast); --colour-shadow: var(--colour-contrast); + --colour-icon: var(--colour-text); + --colour-icon-alt: var(--colour-brick); --pattern-light: var(--colour-background); --pattern-dark: var(--colour-steel); @@ -134,6 +136,8 @@ --spacing-lg: 2rem; --spacing-xl: 4rem; + --size-icon: var(--spacing-lg); + /* Shadows */ --shadow-sm: 3px 3px 0 var(--colour-shadow); --shadow-md: 5px 5px 0 var(--colour-shadow); @@ -160,6 +164,13 @@ hsl(360deg 100% 60%) ); } + + /* Shared Component Styles */ + .icon { + width: var(--size-icon); + height: var(--size-icon); + aspect-ratio: 1; + } </style> `} </svelte:head> diff --git a/src/lib/utilTypes.ts b/src/lib/utilTypes.ts new file mode 100644 index 0000000000000000000000000000000000000000..91afa08332caa17527a42212166f4219489691bb --- /dev/null +++ b/src/lib/utilTypes.ts @@ -0,0 +1 @@ +export type BarbiClass = string | string[] | Record<string, boolean> | BarbiClass[] | null | false; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ce1ad1307b4ce20bceb3ccd52809fafed1b7a7e6..fb0d6e86a2f6cf2c290b0415cacf6614e2212d80 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,21 +1,35 @@ <script> - import BarbiTheme from '$lib/theme/BarbiTheme.svelte'; - import Panel from '$lib/atoms/Panel.svelte'; + import { Button, Popover, Anchor, Panel, BarbiTheme } from '$lib'; + import AngleDownIcon from '$lib/patterns/icons/AngleDownIcon.svelte'; + import Dropdown from '$lib/components/modal/Dropdown.svelte'; </script> <BarbiTheme /> - -<div style="background-color: var(--colour-primary); width: 100vw; height: 100vh"> - <Panel tag="a" space="sm" class="panel-navbar" hover>Cool stuff</Panel> +<div class="sss"> + <Dropdown + label="This is a dropdown" + options={[ + { + label: 'First Option', + value: '123', + onClick() { + console.log("FOO") + } + }, + { label: 'Second Option', value: '456', href: 'https://example.com' } + ]} + /> </div> -<h1>Welcome to your library project</h1> -<p>Create your package using @sveltejs/package and preview/showcase your work with SvelteKit</p> -<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p> - -<div>foo</div> - <style> + .sss { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } :global { .panel-navbar { /*width: 100%;*/