From d340105ce6d68aefa44a02deea7a60f9b6ec9a17 Mon Sep 17 00:00:00 2001 From: fabiangaukler <48679357+fabiangaukler@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:54:16 +0100 Subject: [PATCH] feat: initial setup of button classes (#48) Co-authored-by: Fabian Gaukler --- .changeset/rare-rings-count.md | 6 + package-lock.json | 42 ++- package.json | 1 + packages/documentation/.storybook/main.ts | 4 +- .../elements/button/button-story-helpers.ts | 91 ++++++ .../src/elements/button/button.stories.ts | 26 ++ .../src/elements/button/buttons.mdx | 24 ++ .../foundation/src/atomic-playfulness.css | 3 + packages/foundation/src/elements/base.css | 5 + packages/foundation/src/elements/button.css | 272 ++++++++++++++++++ packages/foundation/tokens.config.mjs | 32 ++- packages/foundation/tsconfig.json | 7 + 12 files changed, 507 insertions(+), 6 deletions(-) create mode 100644 .changeset/rare-rings-count.md create mode 100644 packages/documentation/src/elements/button/button-story-helpers.ts create mode 100644 packages/documentation/src/elements/button/button.stories.ts create mode 100644 packages/documentation/src/elements/button/buttons.mdx create mode 100644 packages/foundation/src/elements/base.css create mode 100644 packages/foundation/src/elements/button.css create mode 100644 packages/foundation/tsconfig.json diff --git a/.changeset/rare-rings-count.md b/.changeset/rare-rings-count.md new file mode 100644 index 0000000..08886fa --- /dev/null +++ b/.changeset/rare-rings-count.md @@ -0,0 +1,6 @@ +--- +"@holisticon/hap-documentation": minor +"@holisticon/hap-foundation": minor +--- + +Added buttons and styling for them diff --git a/package-lock.json b/package-lock.json index c743929..95afb59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "lint-staged": "^15.2.4", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", + "storybook-addon-pseudo-states": "4.0.2", "typescript": "^5.4.5", "typescript-eslint": "^8.0.0-alpha.16" } @@ -2304,6 +2305,7 @@ "os": [ "aix" ], + "peer": true, "engines": { "node": ">=12" } @@ -2320,6 +2322,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -2336,6 +2339,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -2352,6 +2356,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=12" } @@ -2368,6 +2373,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -2384,6 +2390,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=12" } @@ -2400,6 +2407,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2416,6 +2424,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2432,6 +2441,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2448,6 +2458,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2464,6 +2475,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2480,6 +2492,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2496,6 +2509,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2512,6 +2526,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2528,6 +2543,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2544,6 +2560,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2560,6 +2577,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=12" } @@ -2576,6 +2594,7 @@ "os": [ "netbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2592,6 +2611,7 @@ "os": [ "openbsd" ], + "peer": true, "engines": { "node": ">=12" } @@ -2608,6 +2628,7 @@ "os": [ "sunos" ], + "peer": true, "engines": { "node": ">=12" } @@ -2624,6 +2645,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -2640,6 +2662,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -2656,6 +2679,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=12" } @@ -3640,9 +3664,9 @@ "dev": true }, "node_modules/@storybook/icons": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.2.9.tgz", - "integrity": "sha512-cOmylsz25SYXaJL/gvTk/dl3pyk7yBFRfeXTsHvTA3dfhoU/LWSq0NKL9nM7WBasJyn6XPSGnLS4RtKXLw5EUg==", + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.2.12.tgz", + "integrity": "sha512-UxgyK5W3/UV4VrI3dl6ajGfHM4aOqMAkFLWe2KibeQudLf6NJpDrDMSHwZj+3iKC4jFU7dkKbbtH2h/al4sW3Q==", "dev": true, "engines": { "node": ">=14.0.0" @@ -9922,6 +9946,18 @@ "url": "https://opencollective.com/storybook" } }, + "node_modules/storybook-addon-pseudo-states": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/storybook-addon-pseudo-states/-/storybook-addon-pseudo-states-4.0.2.tgz", + "integrity": "sha512-dTHkeq4VzqIrJfR8k2BHkX190cQ+aYBZUktWNZpDw3N5tJ8IwaRLzf6ZjHDHY6xwDshaNiTO5UkziRE0Ytukkw==", + "dev": true, + "dependencies": { + "@storybook/icons": "^1.2.10" + }, + "peerDependencies": { + "storybook": "^8.2.0" + } + }, "node_modules/storybook/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", diff --git a/package.json b/package.json index 923c60b..514ffab 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "lint-staged": "^15.2.4", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^3.2.4", + "storybook-addon-pseudo-states": "4.0.2", "typescript": "^5.4.5", "typescript-eslint": "^8.0.0-alpha.16" } diff --git a/packages/documentation/.storybook/main.ts b/packages/documentation/.storybook/main.ts index b700949..a5f4f76 100644 --- a/packages/documentation/.storybook/main.ts +++ b/packages/documentation/.storybook/main.ts @@ -3,8 +3,8 @@ import type { StorybookConfig } from "@storybook/web-components-vite"; const config: StorybookConfig = { framework: "@storybook/web-components-vite", core: { disableTelemetry: true }, - stories: ["../src/**/*.mdx"], - addons: ["@storybook/addon-links", "@storybook/addon-essentials"], + stories: ["../src/**/*.mdx", "../src/**/*.stories.ts"], + addons: ["@storybook/addon-essentials", "storybook-addon-pseudo-states"], }; export default config; diff --git a/packages/documentation/src/elements/button/button-story-helpers.ts b/packages/documentation/src/elements/button/button-story-helpers.ts new file mode 100644 index 0000000..1a3ca6e --- /dev/null +++ b/packages/documentation/src/elements/button/button-story-helpers.ts @@ -0,0 +1,91 @@ +import type { ArgTypes } from "@storybook/web-components"; +import { html } from "lit"; + +type ButtonSize = "default" | "small"; +type ButtonVariant = "primary" | "secondary" | "tertiary" | "destructive"; +type ButtonIconPosition = "left" | "right"; +type ColorScheme = "light" | "dark"; + +export const buttonArgs = (variant: ButtonVariant): ButtonArgs => ({ + variant, + size: "default", + disabled: false, + iconPosition: "left", + label: "", +}); + +export const buttonArgTypes: Partial> = { + variant: { + control: { type: "select" }, + options: ["primary", "secondary", "tertiary", "destructive"], + }, + size: { + control: { type: "select" }, + options: ["default", "small"], + }, +}; + +export interface ButtonArgs { + label: string; + variant: ButtonVariant; + size: ButtonSize; + disabled: boolean; + icon?: string; + iconPosition: ButtonIconPosition; +} + +export const renderButtons = (args: ButtonArgs, colorScheme: ColorScheme) => + html`
+ ${renderButton({ ...args, size: "default", label: "Default" })} + ${renderButton({ + ...args, + size: "default", + label: "Default", + icon: "[Icon]", + })} + ${renderButton({ + ...args, + size: "default", + label: "Default", + icon: "[Icon]", + iconPosition: "right", + })} + ${renderButton({ ...args, size: "small", label: "Small" })} + ${renderButton({ + ...args, + size: "small", + label: "Small", + icon: "[Icon]", + })} + ${renderButton({ + ...args, + size: "small", + label: "Small", + icon: "[Icon]", + iconPosition: "right", + })} +
`; + +const renderButton = (args: ButtonArgs) => { + return html` + + `; +}; diff --git a/packages/documentation/src/elements/button/button.stories.ts b/packages/documentation/src/elements/button/button.stories.ts new file mode 100644 index 0000000..eb77ba8 --- /dev/null +++ b/packages/documentation/src/elements/button/button.stories.ts @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from "@storybook/web-components"; +import { html } from "lit"; +import { + buttonArgs, + buttonArgTypes, + renderButtons, + type ButtonArgs, +} from "./button-story-helpers.js"; + +const meta: Meta = { + argTypes: buttonArgTypes, + + render: (args) => + html`
+ ${renderButtons(args, "light")}${renderButtons(args, "dark")} +
`, +}; + +export default meta; + +export type Story = StoryObj; + +export const ButtonPrimary: Story = { args: buttonArgs("primary") }; +export const ButtonSecondary: Story = { args: buttonArgs("secondary") }; +export const ButtonTertiary: Story = { args: buttonArgs("tertiary") }; +export const ButtonDestructive: Story = { args: buttonArgs("destructive") }; diff --git a/packages/documentation/src/elements/button/buttons.mdx b/packages/documentation/src/elements/button/buttons.mdx new file mode 100644 index 0000000..23090f1 --- /dev/null +++ b/packages/documentation/src/elements/button/buttons.mdx @@ -0,0 +1,24 @@ +import { Meta, Canvas } from "@storybook/blocks"; +import * as ButtonStories from "./button.stories.ts"; + + + +# Buttons + +The following kinds of buttons are available. + +## Primary Button + + + +## Secondary Button + + + +## Tertiary Button + + + +## Destructive Button + + diff --git a/packages/foundation/src/atomic-playfulness.css b/packages/foundation/src/atomic-playfulness.css index 43d7588..b492501 100644 --- a/packages/foundation/src/atomic-playfulness.css +++ b/packages/foundation/src/atomic-playfulness.css @@ -1,3 +1,6 @@ +@import "./elements/base.css"; + @import "../dist/tokens.css"; @import "./es-klarheit.css"; @import "./elements/typography.css"; +@import "./elements/button.css"; diff --git a/packages/foundation/src/elements/base.css b/packages/foundation/src/elements/base.css new file mode 100644 index 0000000..6cb2fe0 --- /dev/null +++ b/packages/foundation/src/elements/base.css @@ -0,0 +1,5 @@ +:root { + color-scheme: light dark; + /* arbitrary background color for storybook */ + background-color: white; +} diff --git a/packages/foundation/src/elements/button.css b/packages/foundation/src/elements/button.css new file mode 100644 index 0000000..9f62b4d --- /dev/null +++ b/packages/foundation/src/elements/button.css @@ -0,0 +1,272 @@ +.hap-button { + font-family: var(--hap-typography-font-family-body); + font-size: var(--hap-typography-font-size-body-small); + font-weight: var(--hap-typography-font-weight-medium); + letter-spacing: var(--hap-typography-letter-spacing-lg); + line-height: var(--hap-typography-line-height-body-regular-singleline); + border: none; + border-radius: var(--hap-radius-radius-full); + padding: var(--hap-spacing-sm) var(--hap-spacing-md); + cursor: pointer; + width: max-content; + + &.small { + padding: var(--hap-spacing-xs) var(--hap-spacing-sm); + font-size: var(--hap-typography-font-size-body-small); + } + + &:disabled { + opacity: var(--hap-opacity-disabled); + cursor: not-allowed; + } + + &:focus, + &:focus-visible { + outline: none; + } + + &.primary { + color: light-dark( + var(--hap-color-text-brand-ondark), + var(--hap-color-text-brand-onlight) + ); + background-color: light-dark( + var(--hap-color-button-primary-onlight-default), + var(--hap-color-button-primary-ondark-default) + ); + + &:hover { + color: light-dark( + var(--hap-color-text-brand-ondark), + var(--hap-color-text-brand-onlight) + ); + background-color: light-dark( + var(--hap-color-button-primary-onlight-hovered), + var(--hap-color-button-primary-ondark-hovered) + ); + } + + &:focus, + &:focus-visible { + color: light-dark( + var(--hap-color-text-brand-ondark), + var(--hap-color-text-brand-onlight) + ); + background-color: light-dark( + var(--hap-color-button-primary-onlight-default), + var(--hap-color-button-primary-ondark-default) + ); + box-shadow: inset 0 0 0 2px + light-dark( + var(--hap-color-border-focused-onlight), + var(--hap-color-border-focused-ondark) + ); + } + + &:active { + color: light-dark( + var(--hap-color-text-brand-ondark), + var(--hap-color-text-brand-onlight) + ); + background-color: light-dark( + var(--hap-color-button-primary-onlight-pressed), + var(--hap-color-button-primary-ondark-pressed) + ); + } + + &:disabled { + color: light-dark( + var(--hap-color-text-brand-ondark), + var(--hap-color-text-brand-onlight) + ); + background-color: light-dark( + var(--hap-color-button-primary-onlight-default), + var(--hap-color-button-primary-ondark-default) + ); + } + } + + &.secondary { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondark) + ); + background-color: none; + box-shadow: inset 0 0 0 1px + light-dark( + var(--hap-color-border-brand-onlight), + var(--hap-color-border-brand-ondarkonly) + ); + + &:hover { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + background-color: light-dark( + var(--hap-color-interactions-onlight-hovered), + var(--hap-color-interactions-ondarkonly-hovered) + ); + box-shadow: inset 0 0 0 1px + light-dark( + var(--hap-color-border-brand-onlight), + var(--hap-color-border-brand-ondarkonly) + ); + } + + &:focus, + &:focus-visible { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + box-shadow: inset 0 0 0 2px + light-dark( + var(--hap-color-border-focused-onlight), + var(--hap-color-border-focused-ondark) + ); + } + + &:active { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + background-color: light-dark( + var(--hap-color-interactions-onlight-pressed), + var(--hap-color-interactions-ondarkonly-pressed) + ); + box-shadow: inset 0 0 0 1px + light-dark( + var(--hap-color-border-brand-onlight), + var(--hap-color-border-brand-ondarkonly) + ); + } + + &:disabled { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + background-color: none; + box-shadow: inset 0 0 0 1px + light-dark( + var(--hap-color-border-brand-onlight), + var(--hap-color-border-brand-ondarkonly) + ); + } + } + + /* same as secondary but without borders in some states */ + &.tertiary { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondark) + ); + background-color: none; + + &:hover { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + background-color: light-dark( + var(--hap-color-interactions-onlight-hovered), + var(--hap-color-interactions-ondarkonly-hovered) + ); + } + + &:focus, + &:focus-visible { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + box-shadow: inset 0 0 0 2px + light-dark( + var(--hap-color-border-focused-onlight), + var(--hap-color-border-focused-ondark) + ); + } + + &:active { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + background-color: light-dark( + var(--hap-color-interactions-onlight-pressed), + var(--hap-color-interactions-ondarkonly-pressed) + ); + } + + &:disabled { + color: light-dark( + var(--hap-color-text-brand-onlight), + var(--hap-color-text-brand-ondarkonly) + ); + background-color: none; + } + } + + &.destructive { + color: light-dark( + var(--hap-color-feedback-critical-light), + var(--hap-color-feedback-critical-light) + ); + background-color: light-dark( + var(--hap-color-button-destructive-onlight-default), + var(--hap-color-button-destructive-ondark-default) + ); + + &:hover { + color: light-dark( + var(--hap-color-feedback-critical-light), + var(--hap-color-feedback-critical-light) + ); + background-color: light-dark( + var(--hap-color-button-destructive-onlight-hovered), + var(--hap-color-button-destructive-ondark-hovered) + ); + } + + &:focus, + &:focus-visible { + color: light-dark( + var(--hap-color-feedback-critical-light), + var(--hap-color-feedback-critical-light) + ); + background-color: light-dark( + var(--hap-color-button-destructive-onlight-hovered), + var(--hap-color-button-destructive-ondark-hovered) + ); + box-shadow: inset 0 0 0 2px + light-dark( + var(--hap-color-border-focused-onlight), + var(--hap-color-border-focused-ondark) + ); + } + + &:active { + color: light-dark( + var(--hap-color-feedback-critical-light), + var(--hap-color-feedback-critical-light) + ); + background-color: light-dark( + var(--hap-color-button-destructive-onlight-pressed), + var(--hap-color-button-destructive-ondark-pressed) + ); + } + + &:disabled { + color: light-dark( + var(--hap-color-feedback-critical-light), + var(--hap-color-feedback-critical-light) + ); + background-color: light-dark( + var(--hap-color-button-destructive-onlight-default), + var(--hap-color-button-destructive-ondark-default) + ); + } + } +} diff --git a/packages/foundation/tokens.config.mjs b/packages/foundation/tokens.config.mjs index 33b970e..378f3ad 100644 --- a/packages/foundation/tokens.config.mjs +++ b/packages/foundation/tokens.config.mjs @@ -17,7 +17,37 @@ export default { .toLowerCase() .replace("hap-tokens", "hap"); }, prefix), - transform: (token) => (token.$type === "string" ? token.value : void 0), + transform: (token) => { + if (token.$type === "string") { + return token.value; + } + + // opacity values in figma range from 0 to 100 + if ( + token.$type === "dimension" && + token.id.toLowerCase().includes("opacity") + ) { + const sanitizedOpacity = Number( + token.$value.replace(/[^\d,.]+/g, ""), + ).toFixed(2); + + return Number(sanitizedOpacity) / 100; + } + + // letter-spacing values in figma are decimals + if ( + token.$type === "dimension" && + token.id.toLowerCase().includes("letter-spacing") + ) { + const sanitizedLetterSpacing = Number( + token.$value.replace(/[^\d-,.]+/g, ""), + ).toFixed(2); + + return Number(sanitizedLetterSpacing); + } + + return void 0; + }, }), ], }; diff --git a/packages/foundation/tsconfig.json b/packages/foundation/tsconfig.json new file mode 100644 index 0000000..c213720 --- /dev/null +++ b/packages/foundation/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "experimentalDecorators": true, + "useDefineForClassFields": false + } +}