diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c93fe64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +storybook-static/ +build-storybook.log +.DS_Store +.env +.yarn +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..53c37a1 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.storybook/local-preset.js b/.storybook/local-preset.js new file mode 100644 index 0000000..01ddea9 --- /dev/null +++ b/.storybook/local-preset.js @@ -0,0 +1,15 @@ +/** + * to load the built addon in this test Storybook + */ +function previewAnnotations(entry = []) { + return [...entry, require.resolve("../dist/preview.mjs")]; +} + +function managerEntries(entry = []) { + return [...entry, require.resolve("../dist/manager.mjs")]; +} + +module.exports = { + managerEntries, + previewAnnotations, +}; \ No newline at end of file diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..c996812 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,18 @@ +import type { StorybookConfig } from "@storybook/react-vite"; +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + "./local-preset.js", + ], + framework: { + name: "@storybook/react-vite", + options: {}, + }, + docs: { + autodocs: "tag", + }, +}; +export default config; \ No newline at end of file diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..05da1e9 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..7f0feb3 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,18 @@ +import type { Preview } from "@storybook/react"; + +const preview: Preview = { + parameters: { + backgrounds: { + default: "light", + }, + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +}; + +export default preview; \ No newline at end of file diff --git a/LICENSE b/LICENSE index cf71476..912ce7a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,21 @@ MIT License -Copyright (c) 2024 Valkyriecoms +Copyright (c) 2023 Storybook contributors -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index e526d45..5016be0 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,92 @@ -# storybook-addon-valkyrie-theme +# Storybook Addon Valkyrie Theme +A Storybook addon for applying Valkyrie themes -A Storybook addon for applying Valkyrie themes \ No newline at end of file +### Development scripts + +- `yarn start` runs babel in watch mode and starts Storybook +- `yarn build` build and package your addon code + +### Switch from TypeScript to JavaScript + +Don't want to use TypeScript? We offer a handy eject command: `yarn eject-ts` + +This will convert all code to JS. It is a destructive process, so we recommended running this before you start writing any code. + +## What's included? + +![Demo](https://user-images.githubusercontent.com/42671/107857205-e7044380-6dfa-11eb-8718-ad02e3ba1a3f.gif) + +The addon code lives in `src`. It demonstrates all core addon related concepts. The three [UI paradigms](https://storybook.js.org/docs/react/addons/addon-types#ui-based-addons) + +- `src/Tool.tsx` +- `src/Panel.tsx` +- `src/Tab.tsx` + +Which, along with the addon itself, are registered in `src/manager.ts`. + +Managing State and interacting with a story: + +- `src/withGlobals.ts` & `src/Tool.tsx` demonstrates how to use `useGlobals` to manage global state and modify the contents of a Story. +- `src/withRoundTrip.ts` & `src/Panel.tsx` demonstrates two-way communication using channels. +- `src/Tab.tsx` demonstrates how to use `useParameter` to access the current story's parameters. + +Your addon might use one or more of these patterns. Feel free to delete unused code. Update `src/manager.ts` and `src/preview.ts` accordingly. + +Lastly, configure you addon name in `src/constants.ts`. + +### Metadata + +Storybook addons are listed in the [catalog](https://storybook.js.org/addons) and distributed via npm. The catalog is populated by querying npm's registry for Storybook-specific metadata in `package.json`. This project has been configured with sample data. Learn more about available options in the [Addon metadata docs](https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata). + +## Release Management + +### Setup + +This project is configured to use [auto](https://github.com/intuit/auto) for release management. It generates a changelog and pushes it to both GitHub and npm. Therefore, you need to configure access to both: + +- [`NPM_TOKEN`](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-access-tokens) Create a token with both _Read and Publish_ permissions. +- [`GH_TOKEN`](https://github.com/settings/tokens) Create a token with the `repo` scope. + +Then open your `package.json` and edit the following fields: + +- `name` +- `author` +- `repository` + +#### Local + +To use `auto` locally create a `.env` file at the root of your project and add your tokens to it: + +```bash +GH_TOKEN= +NPM_TOKEN= +``` + +Lastly, **create labels on GitHub**. You’ll use these labels in the future when making changes to the package. + +```bash +npx auto create-labels +``` + +If you check on GitHub, you’ll now see a set of labels that `auto` would like you to use. Use these to tag future pull requests. + +#### GitHub Actions + +This template comes with GitHub actions already set up to publish your addon anytime someone pushes to your repository. + +Go to `Settings > Secrets`, click `New repository secret`, and add your `NPM_TOKEN`. + +### Creating a release + +To create a release locally you can run the following command, otherwise the GitHub action will make the release for you. + +```sh +yarn release +``` + +That will: + +- Build and package the addon code +- Bump the version +- Push a release to GitHub and npm +- Push a changelog to GitHub \ No newline at end of file diff --git a/manager.js b/manager.js new file mode 100644 index 0000000..13132a6 --- /dev/null +++ b/manager.js @@ -0,0 +1 @@ +export * from "./dist/manager"; \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..de2ef66 --- /dev/null +++ b/package.json @@ -0,0 +1,134 @@ +{ + "name": "storybook-addon-valkyrie-theme", + "version": "0.0.0", + "description": "A Storybook addon for applying Valkyrie themes", + "keywords": [ + "storybook-addons", + "storybook-addons" + ], + "repository": { + "type": "git", + "url": "https://toastielab.dev/Valkyriecoms/storybook-addon-valkyrie-theme.git" + }, + "author": "Toastie ", + "license": "MIT", + "exports": { + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs", + "types": "./dist/index.d.ts" + }, + "./manager": { + "require": "./dist/manager.js", + "import": "./dist/manager.mjs", + "types": "./dist/manager.d.ts" + }, + "./manager.js": { + "require": "./dist/manager.js", + "import": "./dist/manager.mjs", + "types": "./dist/manager.d.ts" + }, + "./preview": { + "require": "./dist/preview.js", + "import": "./dist/preview.mjs", + "types": "./dist/preview.d.ts" + }, + "./preview.js": { + "require": "./dist/preview.js", + "import": "./dist/preview.mjs", + "types": "./dist/preview.d.ts" + }, + "./package.json": "./package.json" + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*", + "README.md", + "*.js", + "*.d.ts" + ], + "scripts": { + "clean": "rimraf ./dist", + "prebuild": "yarn clean", + "build": "tsup", + "build:watch": "yarn build --watch", + "test": "echo \"Error: no test specified\" && exit 1", + "start": "concurrently \"yarn build:watch\" \"yarn storybook --quiet --no-open\"", + "prerelease": "zx scripts/prepublish-checks.mjs", + "release": "yarn build && auto shipit", + "eject-ts": "zx scripts/eject-typescript.mjs", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "devDependencies": { + "@storybook/addon-essentials": "^7.0.0-rc.4", + "@storybook/addon-interactions": "^7.0.0-rc.4", + "@storybook/addon-links": "^7.0.0-rc.4", + "@storybook/blocks": "^7.0.0-rc.4", + "@storybook/components": "^7.0.0-rc.4", + "@storybook/core-events": "^7.0.0-rc.4", + "@storybook/manager-api": "^7.0.0-rc.4", + "@storybook/preview-api": "^7.0.0-rc.4", + "@storybook/react": "^7.0.0-rc.4", + "@storybook/react-vite": "^7.0.0-rc.4", + "@storybook/testing-library": "^0.0.14-next.1", + "@storybook/theming": "^7.0.0-rc.4", + "@storybook/types": "^7.0.0-rc.4", + "@types/node": "^18.15.0", + "@vitejs/plugin-react": "^3.1.0", + "auto": "^10.3.0", + "boxen": "^5.0.1", + "concurrently": "^6.2.0", + "dedent": "^0.7.0", + "prettier": "^2.3.1", + "prompts": "^2.4.2", + "prop-types": "^15.8.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "rimraf": "^3.0.2", + "storybook": "^7.0.0-rc.4", + "tsup": "^6.6.3", + "typescript": "^4.9.5", + "vite": "^4.1.4", + "zx": "^1.14.1" + }, + "peerDependencies": { + "@storybook/blocks": "^7.0.0-rc.4", + "@storybook/components": "^7.0.0-rc.4", + "@storybook/core-events": "^7.0.0-rc.4", + "@storybook/manager-api": "^7.0.0-rc.4", + "@storybook/preview-api": "^7.0.0-rc.4", + "@storybook/theming": "^7.0.0-rc.4", + "@storybook/types": "^7.0.0-rc.4", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "publishConfig": { + "access": "public" + }, + "storybook": { + "displayName": "Valkyrie Theme", + "supportedFrameworks": [ + "react", + "vue", + "angular", + "web-components", + "ember", + "html", + "svelte", + "preact" + ], + "icon": "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png" + }, + "packageManager": "yarn@1.22.19" +} diff --git a/preview.js b/preview.js new file mode 100644 index 0000000..a0f2556 --- /dev/null +++ b/preview.js @@ -0,0 +1 @@ +export * from "./dist/preview"; \ No newline at end of file diff --git a/scripts/eject-typescript.mjs b/scripts/eject-typescript.mjs new file mode 100644 index 0000000..32e0fe6 --- /dev/null +++ b/scripts/eject-typescript.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env zx + +// Copy TS files and delete src +await $`cp -r ./src ./srcTS`; +await $`rm -rf ./src`; +await $`mkdir ./src`; + +// Convert TS code to JS +await $`babel --no-babelrc --presets @babel/preset-typescript ./srcTS -d ./src --extensions \".js,.jsx,.ts,.tsx\" --ignore "./srcTS/typings.d.ts"`; + +// Format the newly created .js files +await $`prettier --write ./src`; + +// Add in minimal files required for the TS build setup +await $`touch ./src/dummy.ts`; +await $`printf "export {};" >> ./src/dummy.ts`; + +await $`touch ./src/typings.d.ts`; +await $`printf 'declare module "global";' >> ./src/typings.d.ts`; + +// Clean up +await $`rm -rf ./srcTS`; + +console.log( + chalk.green.bold` +TypeScript Ejection complete!`, + chalk.green` +Addon code converted with JS. The TypeScript build setup is still available in case you want to adopt TypeScript in the future. +` +); \ No newline at end of file diff --git a/scripts/prepublish-checks.mjs b/scripts/prepublish-checks.mjs new file mode 100644 index 0000000..cdbe6f7 --- /dev/null +++ b/scripts/prepublish-checks.mjs @@ -0,0 +1,56 @@ +#!/usr/bin/env zx + +const packageJson = require("../package.json"); +const boxen = require("boxen"); +const dedent = require("dedent"); + +const name = packageJson.name; +const displayName = packageJson.storybook.displayName; + +let exitCode = 0; +$.verbose = false; + +/** + * Check that meta data has been updated + */ +if (name.includes("addon-kit") || displayName.includes("Addon Kit")) { + console.error( + boxen( + dedent` + ${chalk.red.bold("Missing metadata")} + + ${chalk.red(dedent`Your package name and/or displayName includes default values from the Addon Kit. + The addon gallery filters out all such addons. + + Please configure appropriate metadata before publishing your addon. For more info, see: + https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata`)}`, + { padding: 1, borderColor: "red" } + ) + ); + + exitCode = 1; +} + +/** + * Check that README has been updated + */ +const readmeTestStrings = + "# Storybook Addon Kit|Click the \\*\\*Use this template\\*\\* button to get started.|https://user-images.githubusercontent.com/42671/106809879-35b32000-663a-11eb-9cdc-89f178b5273f.gif"; + +if ((await $`cat README.md | grep -E ${readmeTestStrings}`.exitCode) == 0) { + console.error( + boxen( + dedent` + ${chalk.red.bold("README not updated")} + + ${chalk.red(dedent`You are using the default README.md file that comes with the addon kit. + Please update it to provide info on what your addon does and how to use it.`)} + `, + { padding: 1, borderColor: "red" } + ) + ); + + exitCode = 1; +} + +process.exit(exitCode); \ No newline at end of file diff --git a/scripts/welcome.js b/scripts/welcome.js new file mode 100644 index 0000000..0420ddc --- /dev/null +++ b/scripts/welcome.js @@ -0,0 +1,180 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable no-console */ +const prompts = require("prompts"); +const dedent = require("ts-dedent").default; +const path = require("path"); +const fs = require("fs"); +const { execSync } = require("child_process"); + +// CLI questions +const questions = [ + { + type: "text", + name: "authorName", + initial: "", + message: "What is the package author name?*", + validate: (name) => (name === "" ? "Name can't be empty" : true), + }, + { + type: "text", + name: "authorEmail", + initial: "", + message: "What is the package author email?", + }, + { + type: "text", + name: "packageName", + message: "What is the addon package name (eg: storybook-addon-something)?*", + validate: (name) => (name === "" ? "Package name can't be empty" : true), + }, + { + type: "text", + name: "displayName", + message: + "What is the addon display name (this will be used in the addon catalog)?*", + validate: (name) => + name === "" + ? "Display name can't be empty. For more info, see: https://storybook.js.org/docs/react/addons/addon-catalog#addon-metadata" + : true, + }, + { + type: "text", + name: "addonDescription", + initial: "", + message: "Write a short description of the addon*", + validate: (name) => (name === "" ? "Description can't be empty" : true), + }, + { + type: "text", + name: "repoUrl", + message: "Git repo URL for your addon package (https://github.com/...)*", + validate: (url) => (url === "" ? "URL can't be empty" : true), + }, + { + type: "text", + name: "addonIcon", + initial: + "https://user-images.githubusercontent.com/321738/63501763-88dbf600-c4cc-11e9-96cd-94adadc2fd72.png", + message: "URL of your addon icon", + }, + { + type: "list", + name: "keywords", + initial: "storybook-addons", + message: "Enter addon keywords (comma separated)", + separator: ",", + format: (keywords) => + keywords + .concat(["storybook-addons"]) + .map((k) => `"${k}"`) + .join(", "), + }, + { + type: "list", + name: "supportedFrameworks", + initial: + "react, vue, angular, web-components, ember, html, svelte, preact, react-native", + message: "List of frameworks you support (comma separated)?", + separator: ",", + format: (frameworks) => frameworks.map((k) => `"${k}"`).join(", "), + }, +]; + +const REPLACE_TEMPLATES = { + packageName: "storybook-addon-kit", + addonDescription: "everything you need to build a Storybook addon", + packageAuthor: "package-author", + repoUrl: "https://github.com/storybookjs/storybook-addon-kit", + keywords: `"storybook-addons"`, + displayName: "Addon Kit", + supportedFrameworks: `"supported-frameworks"`, +}; + +const bold = (message) => `\u001b[1m${message}\u001b[22m`; +const magenta = (message) => `\u001b[35m${message}\u001b[39m`; +const blue = (message) => `\u001b[34m${message}\u001b[39m`; + +const main = async () => { + console.log( + bold( + magenta( + dedent` + Welcome to Storybook addon-kit! + Please answer the following questions while we prepare this project for you:\n + ` + ) + ) + ); + + const { + authorName, + authorEmail, + packageName, + addonDescription, + repoUrl, + displayName, + keywords, + supportedFrameworks, + } = await prompts(questions); + + if (!authorName || !packageName) { + console.log( + `\nProcess canceled by the user. Feel free to run ${bold( + "yarn postinstall" + )} to execute the installation steps again!` + ); + process.exit(0); + } + + const authorField = authorName + (authorEmail ? ` <${authorEmail}>` : ""); + + const packageJson = path.resolve(__dirname, `../package.json`); + + console.log(`\n👷 Updating package.json...`); + let packageJsonContents = fs.readFileSync(packageJson, "utf-8"); + + packageJsonContents = packageJsonContents + .replace(REPLACE_TEMPLATES.packageName, packageName) + .replace(REPLACE_TEMPLATES.addonDescription, addonDescription) + .replace(REPLACE_TEMPLATES.packageAuthor, authorField) + .replace(REPLACE_TEMPLATES.keywords, keywords) + .replace(REPLACE_TEMPLATES.repoUrl, repoUrl) + .replace(REPLACE_TEMPLATES.displayName, displayName) + .replace(REPLACE_TEMPLATES.supportedFrameworks, supportedFrameworks) + .replace(/\s*"postinstall".*node.*scripts\/welcome.js.*",/, ''); + + fs.writeFileSync(packageJson, packageJsonContents); + + console.log("📝 Updating the README..."); + const readme = path.resolve(__dirname, `../README.md`); + let readmeContents = fs.readFileSync(readme, "utf-8"); + + const regex = /<\!-- README START -->([\s\S]*)<\!-- README END -->/g; + + readmeContents = readmeContents.replace( + regex, + dedent` + # Storybook Addon ${displayName} + ${addonDescription} + ` + ); + + fs.writeFileSync(readme, readmeContents); + + console.log(`📦 Creating a commit...`); + execSync('git add . && git commit -m "project setup" --no-verify'); + + console.log( + dedent`\n + 🚀 All done! Run \`yarn start\` to get started. + + Thanks for using this template, ${authorName.split(" ")[0]}! ❤️ + + Feel free to open issues in case there are bugs/feature requests at: + + ${bold(blue("https://github.com/storybookjs/addon-kit"))}\n + ` + ); +}; + +main().catch((e) => console.log(`Something went wrong: ${e}`)); \ No newline at end of file diff --git a/src/Tool.tsx b/src/Tool.tsx new file mode 100644 index 0000000..d276ab4 --- /dev/null +++ b/src/Tool.tsx @@ -0,0 +1,67 @@ +import React, { memo, useState } from "react"; +import { useGlobals, useStorybookApi } from "@storybook/manager-api"; +import { Icons, IconButton, TooltipLinkList, WithTooltip } from "@storybook/components"; +import { ADDON_ID, PARAM_KEY, TOOL_ID } from "./constants"; + +export const Tool = memo(function MyAddonSelector() { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + const [globals, updateGlobals] = useGlobals(); + const api = useStorybookApi(); + + const isActive = !!globals[PARAM_KEY]; + + return ( + { + return ( + ({ + id: theme, + title: theme, + onClick() { + updateGlobals({ + [PARAM_KEY]: i ? theme : false, + }); + onHide(); + }, + value: theme, + active: theme === globals[PARAM_KEY], + }))} + /> + ); + }} + onVisibleChange={setIsTooltipVisible} + > + + + + + ); +}); \ No newline at end of file diff --git a/src/components/List.tsx b/src/components/List.tsx new file mode 100644 index 0000000..9e75983 --- /dev/null +++ b/src/components/List.tsx @@ -0,0 +1,95 @@ +import React, { Fragment, useState } from "react"; +import { styled, themes, convert } from "@storybook/theming"; +import { Icons, IconsProps } from "@storybook/components"; + +const ListWrapper = styled.ul({ + listStyle: "none", + fontSize: 14, + padding: 0, + margin: 0, +}); + +const Wrapper = styled.div({ + display: "flex", + width: "100%", + borderBottom: `1px solid ${convert(themes.normal).appBorderColor}`, + "&:hover": { + background: convert(themes.normal).background.hoverable, + }, +}); + +const Icon = styled(Icons)({ + height: 10, + width: 10, + minWidth: 10, + color: convert(themes.normal).color.mediumdark, + marginRight: 10, + transition: "transform 0.1s ease-in-out", + alignSelf: "center", + display: "inline-flex", +}); + +const HeaderBar = styled.div({ + padding: convert(themes.normal).layoutMargin, + paddingLeft: convert(themes.normal).layoutMargin - 3, + background: "none", + color: "inherit", + textAlign: "left", + cursor: "pointer", + borderLeft: "3px solid transparent", + width: "100%", + + "&:focus": { + outline: "0 none", + borderLeft: `3px solid ${convert(themes.normal).color.secondary}`, + }, +}); + +const Description = styled.div({ + padding: convert(themes.normal).layoutMargin, + marginBottom: convert(themes.normal).layoutMargin, + fontStyle: "italic", +}); + +type Item = { + title: string; + description: string; +}; + +interface ListItemProps { + item: Item; +} + +export const ListItem: React.FC = ({ item }) => { + const [open, onToggle] = useState(false); + + return ( + + + onToggle(!open)} role="button"> + + {item.title} + + + {open ? {item.description} : null} + + ); +}; + +interface ListProps { + items: Item[]; +} + +export const List: React.FC = ({ items }) => ( + + {items.map((item, idx) => ( + + ))} + +); \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..438fbc6 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,9 @@ +export const ADDON_ID = "valkyriecoms/storybook-addon-valkyrie-theme"; +export const TOOL_ID = `${ADDON_ID}/tool`; +export const PARAM_KEY = `valkyrieTheme`; + +export const EVENTS = { + RESULT: `${ADDON_ID}/result`, + REQUEST: `${ADDON_ID}/request`, + CLEAR: `${ADDON_ID}/clear`, +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..a65711c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +// make it work with --isolatedModules +export default {}; \ No newline at end of file diff --git a/src/manager.ts b/src/manager.ts new file mode 100644 index 0000000..cd8af87 --- /dev/null +++ b/src/manager.ts @@ -0,0 +1,19 @@ +import { addons, types } from "@storybook/manager-api"; +import { ADDON_ID, TOOL_ID } from "./constants"; +import { Tool } from "./Tool"; + +/** + * Note: if you want to use JSX in this file, rename it to `manager.tsx` + * and update the entry prop in tsup.config.ts to use "src/manager.tsx", + */ + +// Register the addon +addons.register(ADDON_ID, () => { + // Register the tool + addons.add(TOOL_ID, { + type: types.TOOL, + title: "Valkyrie theme", + match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), + render: Tool, + }); +}); \ No newline at end of file diff --git a/src/preview.ts b/src/preview.ts new file mode 100644 index 0000000..ebc6a90 --- /dev/null +++ b/src/preview.ts @@ -0,0 +1,27 @@ +/** + * A decorator is a way to wrap a story in extra “rendering” functionality. Many addons define decorators + * in order to augment stories: + * - with extra rendering + * - gather details about how a story is rendered + * + * When writing stories, decorators are typically used to wrap stories with extra markup or context mocking. + * + * https://storybook.js.org/docs/react/writing-stories/decorators + */ +import type { Renderer, ProjectAnnotations } from "@storybook/types"; +import { PARAM_KEY } from "./constants"; +import { withGlobals } from "./withGlobals"; + +/** + * Note: if you want to use JSX in this file, rename it to `preview.tsx` + * and update the entry prop in tsup.config.ts to use "src/preview.tsx", + */ + +const preview: ProjectAnnotations = { + decorators: [withGlobals], + globals: { + [PARAM_KEY]: false, + }, +}; + +export default preview; \ No newline at end of file diff --git a/src/stories/Button.stories.ts b/src/stories/Button.stories.ts new file mode 100644 index 0000000..b9050b1 --- /dev/null +++ b/src/stories/Button.stories.ts @@ -0,0 +1,53 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Button } from "./Button"; + +// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction#default-export +const meta: Meta = { + title: "Example/Button", + component: Button, + // More on argTypes: https://storybook.js.org/docs/react/api/argtypes + argTypes: { + backgroundColor: { control: "color" }, + }, + tags: ["autodocs"], + parameters: { + myAddonParameter: ` + + a.id} /> + +`, + }, +}; + +export default meta; +type Story = StoryObj; + +// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args +export const Primary: Story = { + // More on args: https://storybook.js.org/docs/react/writing-stories/args + args: { + primary: true, + label: "Button", + }, +}; + +export const Secondary: Story = { + args: { + label: "Button", + }, +}; + +export const Large: Story = { + args: { + size: "large", + label: "Button", + }, +}; + +export const Small: Story = { + args: { + size: "small", + label: "Button", + }, +}; \ No newline at end of file diff --git a/src/stories/Button.tsx b/src/stories/Button.tsx new file mode 100644 index 0000000..e56d7f3 --- /dev/null +++ b/src/stories/Button.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import './button.css'; + +interface ButtonProps { + /** + * Is this the principal call to action on the page? + */ + primary?: boolean; + /** + * What background color to use + */ + backgroundColor?: string; + /** + * How large should the button be? + */ + size?: 'small' | 'medium' | 'large'; + /** + * Button contents + */ + label: string; + /** + * Optional click handler + */ + onClick?: () => void; +} + +/** + * Primary UI component for user interaction + */ +export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props +}: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + + ); +}; \ No newline at end of file diff --git a/src/stories/Header.tsx b/src/stories/Header.tsx new file mode 100644 index 0000000..9353107 --- /dev/null +++ b/src/stories/Header.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Button } from './Button'; +import './header.css'; + +type User = { + name: string; +}; + +interface HeaderProps { + user?: User; + onLogin: () => void; + onLogout: () => void; + onCreateAccount: () => void; +} + +export const Header = ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( +
+
+
+ + + + + + + +

Acme

+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+); \ No newline at end of file diff --git a/src/stories/Introduction.mdx b/src/stories/Introduction.mdx new file mode 100644 index 0000000..c581f52 --- /dev/null +++ b/src/stories/Introduction.mdx @@ -0,0 +1,211 @@ +import { Meta } from '@storybook/blocks'; +import Code from './assets/code-brackets.svg'; +import Colors from './assets/colors.svg'; +import Comments from './assets/comments.svg'; +import Direction from './assets/direction.svg'; +import Flow from './assets/flow.svg'; +import Plugin from './assets/plugin.svg'; +import Repo from './assets/repo.svg'; +import StackAlt from './assets/stackalt.svg'; + + + + + +# Welcome to Storybook + +Storybook helps you build UI components in isolation from your app's business logic, data, and context. +That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA. + +Browse example stories now by navigating to them in the sidebar. +View their code in the `stories` directory to learn how they work. +We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages. + +
Configure
+ + + +
Learn
+ + + +
+ TipEdit the Markdown in{' '} + stories/Introduction.stories.mdx +
\ No newline at end of file diff --git a/src/stories/Page.stories.ts b/src/stories/Page.stories.ts new file mode 100644 index 0000000..4bcf3c4 --- /dev/null +++ b/src/stories/Page.stories.ts @@ -0,0 +1,29 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { within, userEvent } from '@storybook/testing-library'; + +import { Page } from './Page'; + +const meta: Meta = { + title: 'Example/Page', + component: Page, + parameters: { + // More on Story layout: https://storybook.js.org/docs/react/configure/story-layout + layout: 'fullscreen', + }, +}; + +export default meta; +type Story = StoryObj; + +export const LoggedOut: Story = {}; + +// More on interaction testing: https://storybook.js.org/docs/react/writing-tests/interaction-testing +export const LoggedIn: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const loginButton = await canvas.getByRole('button', { + name: /Log in/i, + }); + await userEvent.click(loginButton); + }, +}; \ No newline at end of file diff --git a/src/stories/Page.tsx b/src/stories/Page.tsx new file mode 100644 index 0000000..d3c408d --- /dev/null +++ b/src/stories/Page.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import { Header } from './Header'; +import './page.css'; + +type User = { + name: string; +}; + +export const Page: React.FC = () => { + const [user, setUser] = React.useState(); + + return ( +
+
setUser({ name: 'Jane Doe' })} + onLogout={() => setUser(undefined)} + onCreateAccount={() => setUser({ name: 'Jane Doe' })} + /> + +
+

Pages in Storybook

+

+ We recommend building UIs with a{' '} + + component-driven + {' '} + process starting with atomic components and ending with pages. +

+

+ Render pages with mock data. This makes it easy to build and review page states without + needing to navigate to them in your app. Here are some handy patterns for managing page + data in Storybook: +

+
    +
  • + Use a higher-level connected component. Storybook helps you compose such data from the + "args" of child component stories +
  • +
  • + Assemble data in the page component from your services. You can mock these services out + using Storybook. +
  • +
+

+ Get a guided tutorial on component-driven development at{' '} + + Storybook tutorials + + . Read more in the{' '} + + docs + + . +

+
+ Tip Adjust the width of the canvas with the{' '} + + + + + + Viewports addon in the toolbar +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/stories/assets/code-brackets.svg b/src/stories/assets/code-brackets.svg new file mode 100644 index 0000000..73de947 --- /dev/null +++ b/src/stories/assets/code-brackets.svg @@ -0,0 +1 @@ +illustration/code-brackets \ No newline at end of file diff --git a/src/stories/assets/colors.svg b/src/stories/assets/colors.svg new file mode 100644 index 0000000..17d58d5 --- /dev/null +++ b/src/stories/assets/colors.svg @@ -0,0 +1 @@ +illustration/colors \ No newline at end of file diff --git a/src/stories/assets/comments.svg b/src/stories/assets/comments.svg new file mode 100644 index 0000000..6493a13 --- /dev/null +++ b/src/stories/assets/comments.svg @@ -0,0 +1 @@ +illustration/comments \ No newline at end of file diff --git a/src/stories/assets/direction.svg b/src/stories/assets/direction.svg new file mode 100644 index 0000000..65676ac --- /dev/null +++ b/src/stories/assets/direction.svg @@ -0,0 +1 @@ +illustration/direction \ No newline at end of file diff --git a/src/stories/assets/flow.svg b/src/stories/assets/flow.svg new file mode 100644 index 0000000..8ac27db --- /dev/null +++ b/src/stories/assets/flow.svg @@ -0,0 +1 @@ +illustration/flow \ No newline at end of file diff --git a/src/stories/assets/plugin.svg b/src/stories/assets/plugin.svg new file mode 100644 index 0000000..29e5c69 --- /dev/null +++ b/src/stories/assets/plugin.svg @@ -0,0 +1 @@ +illustration/plugin \ No newline at end of file diff --git a/src/stories/assets/repo.svg b/src/stories/assets/repo.svg new file mode 100644 index 0000000..f386ee9 --- /dev/null +++ b/src/stories/assets/repo.svg @@ -0,0 +1 @@ +illustration/repo \ No newline at end of file diff --git a/src/stories/assets/stackalt.svg b/src/stories/assets/stackalt.svg new file mode 100644 index 0000000..9b7ad27 --- /dev/null +++ b/src/stories/assets/stackalt.svg @@ -0,0 +1 @@ +illustration/stackalt \ No newline at end of file diff --git a/src/stories/button.css b/src/stories/button.css new file mode 100644 index 0000000..d83ff2a --- /dev/null +++ b/src/stories/button.css @@ -0,0 +1,30 @@ +.storybook-button { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 700; + border: 0; + border-radius: 3em; + cursor: pointer; + display: inline-block; + line-height: 1; +} +.storybook-button--primary { + color: white; + background-color: #1ea7fd; +} +.storybook-button--secondary { + color: #333; + background-color: transparent; + box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset; +} +.storybook-button--small { + font-size: 12px; + padding: 10px 16px; +} +.storybook-button--medium { + font-size: 14px; + padding: 11px 20px; +} +.storybook-button--large { + font-size: 16px; + padding: 12px 24px; +} \ No newline at end of file diff --git a/src/stories/header.css b/src/stories/header.css new file mode 100644 index 0000000..919eb04 --- /dev/null +++ b/src/stories/header.css @@ -0,0 +1,32 @@ +.wrapper { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + padding: 15px 20px; + display: flex; + align-items: center; + justify-content: space-between; +} + +svg { + display: inline-block; + vertical-align: top; +} + +h1 { + font-weight: 700; + font-size: 20px; + line-height: 1; + margin: 6px 0 6px 10px; + display: inline-block; + vertical-align: top; +} + +button + button { + margin-left: 10px; +} + +.welcome { + color: #333; + font-size: 14px; + margin-right: 10px; +} \ No newline at end of file diff --git a/src/stories/page.css b/src/stories/page.css new file mode 100644 index 0000000..ed0c5ba --- /dev/null +++ b/src/stories/page.css @@ -0,0 +1,69 @@ +section { + font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 24px; + padding: 48px 20px; + margin: 0 auto; + max-width: 600px; + color: #333; +} + +section h2 { + font-weight: 700; + font-size: 32px; + line-height: 1; + margin: 0 0 4px; + display: inline-block; + vertical-align: top; +} + +section p { + margin: 1em 0; +} + +section a { + text-decoration: none; + color: #1ea7fd; +} + +section ul { + padding-left: 30px; + margin: 1em 0; +} + +section li { + margin-bottom: 8px; +} + +section .tip { + display: inline-block; + border-radius: 1em; + font-size: 11px; + line-height: 12px; + font-weight: 700; + background: #e7fdd8; + color: #66bf3c; + padding: 4px 12px; + margin-right: 10px; + vertical-align: top; +} + +section .tip-wrapper { + font-size: 13px; + line-height: 20px; + margin-top: 40px; + margin-bottom: 40px; +} + +section .tip-wrapper svg { + display: inline-block; + height: 12px; + width: 12px; + margin-right: 4px; + vertical-align: top; + margin-top: 3px; +} + +section .tip-wrapper svg path { + fill: #1ea7fd; +} \ No newline at end of file diff --git a/src/withGlobals.ts b/src/withGlobals.ts new file mode 100644 index 0000000..35ce4f7 --- /dev/null +++ b/src/withGlobals.ts @@ -0,0 +1,30 @@ +import type { + Renderer, + PartialStoryFn as StoryFunction, + StoryContext, +} from "@storybook/types"; +import { useEffect, useGlobals } from "@storybook/preview-api"; +import { PARAM_KEY } from "./constants"; + +export const withGlobals = ( + StoryFn: StoryFunction, + context: StoryContext +) => { + const [globals] = useGlobals(); + const state = globals[PARAM_KEY]; + const { theme } = context.globals; + + useEffect(() => { + applyToolState(state); + }, [state, theme]); + + return StoryFn(); +}; + +function applyToolState(state: string | false) { + if (state) { + document.documentElement.dataset.misskeyTheme = state; + } else { + delete document.documentElement.dataset.misskeyTheme; + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..62c74eb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "baseUrl": ".", + "esModuleInterop": true, + "experimentalDecorators": true, + "incremental": false, + "isolatedModules": true, + "jsx": "react", + "lib": ["es2020", "dom"], + "module": "commonjs", + "noImplicitAny": true, + "rootDir": "./src", + "skipLibCheck": true, + "target": "ES2020" + }, + "include": ["src/**/*"] +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..6b92d06 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "tsup"; + +export default defineConfig((options) => ({ + entry: ["src/index.ts", "src/preview.ts", "src/manager.ts"], + splitting: false, + minify: !options.watch, + format: ["cjs", "esm"], + dts: { + resolve: true, + }, + treeshake: true, + sourcemap: true, + clean: true, + platform: "browser", + esbuildOptions(options) { + options.conditions = ["module"]; + }, +})); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..84bbeea --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); \ No newline at end of file