From ccef39fa0c874e58833822547b65aeb64d06ae24 Mon Sep 17 00:00:00 2001 From: Casey Locker Date: Fri, 14 Aug 2026 09:22:16 -0500 Subject: [PATCH 1/3] feat(storybook): add Storybook config, Core UI stories, and Worker config Splits the config and non-MUI stories out of the original combined branch so each PR clears CodeRabbit's 100-file review limit. Co-Authored-By: Claude --- .gitignore | 3 + .npmignore | 4 + .storybook/main.js | 26 + .storybook/preview-head.html | 10 + .storybook/preview.jsx | 44 + package.json | 8 +- stories/_helpers.jsx | 41 + stories/core/ActionDropdown.stories.jsx | 16 + stories/core/AjaxLoader.stories.jsx | 17 + stories/core/AsyncEntityInputs.stories.jsx | 59 + stories/core/BulkActionsSelector.stories.jsx | 20 + stories/core/CheckboxList.stories.jsx | 16 + stories/core/CircleButton.stories.jsx | 20 + stories/core/Clock.stories.jsx | 13 + stories/core/DateTimePicker.stories.jsx | 26 + stories/core/Dropdown.stories.jsx | 18 + stories/core/EditableTable.stories.jsx | 29 + stories/core/Exclusive.stories.jsx | 20 + stories/core/FreeMultiTextInput.stories.jsx | 12 + stories/core/FreeTextSearch.stories.jsx | 10 + stories/core/GroupedDropdown.stories.jsx | 26 + stories/core/Panel.stories.jsx | 17 + stories/core/RadioList.stories.jsx | 17 + stories/core/RawHTML.stories.jsx | 14 + stories/core/RsvpForm.stories.jsx | 17 + stories/core/ScheduleBuilderView.stories.jsx | 70 + stories/core/SelectableTable.stories.jsx | 37 + stories/core/SimpleForm.stories.jsx | 22 + stories/core/SimpleLinkList.stories.jsx | 34 + stories/core/SortableTable.stories.jsx | 33 + stories/core/SteppedSelect.stories.jsx | 16 + stories/core/SummitDaysSelect.stories.jsx | 15 + stories/core/SummitDropdown.stories.jsx | 16 + stories/core/SummitVenuesSelect.stories.jsx | 17 + stories/core/Table.stories.jsx | 36 + stories/core/TextArea.stories.jsx | 13 + stories/core/TextEditor.stories.jsx | 13 + stories/core/TextInput.stories.jsx | 10 + stories/core/UploadInput.stories.jsx | 16 + stories/core/UploadInputV2.stories.jsx | 22 + stories/core/UploadInputV3.stories.jsx | 22 + stories/core/VideoStream.stories.jsx | 10 + wrangler.jsonc | 11 + yarn.lock | 1697 +++++++++++++++++- 44 files changed, 2598 insertions(+), 15 deletions(-) create mode 100644 .storybook/main.js create mode 100644 .storybook/preview-head.html create mode 100644 .storybook/preview.jsx create mode 100644 stories/_helpers.jsx create mode 100644 stories/core/ActionDropdown.stories.jsx create mode 100644 stories/core/AjaxLoader.stories.jsx create mode 100644 stories/core/AsyncEntityInputs.stories.jsx create mode 100644 stories/core/BulkActionsSelector.stories.jsx create mode 100644 stories/core/CheckboxList.stories.jsx create mode 100644 stories/core/CircleButton.stories.jsx create mode 100644 stories/core/Clock.stories.jsx create mode 100644 stories/core/DateTimePicker.stories.jsx create mode 100644 stories/core/Dropdown.stories.jsx create mode 100644 stories/core/EditableTable.stories.jsx create mode 100644 stories/core/Exclusive.stories.jsx create mode 100644 stories/core/FreeMultiTextInput.stories.jsx create mode 100644 stories/core/FreeTextSearch.stories.jsx create mode 100644 stories/core/GroupedDropdown.stories.jsx create mode 100644 stories/core/Panel.stories.jsx create mode 100644 stories/core/RadioList.stories.jsx create mode 100644 stories/core/RawHTML.stories.jsx create mode 100644 stories/core/RsvpForm.stories.jsx create mode 100644 stories/core/ScheduleBuilderView.stories.jsx create mode 100644 stories/core/SelectableTable.stories.jsx create mode 100644 stories/core/SimpleForm.stories.jsx create mode 100644 stories/core/SimpleLinkList.stories.jsx create mode 100644 stories/core/SortableTable.stories.jsx create mode 100644 stories/core/SteppedSelect.stories.jsx create mode 100644 stories/core/SummitDaysSelect.stories.jsx create mode 100644 stories/core/SummitDropdown.stories.jsx create mode 100644 stories/core/SummitVenuesSelect.stories.jsx create mode 100644 stories/core/Table.stories.jsx create mode 100644 stories/core/TextArea.stories.jsx create mode 100644 stories/core/TextEditor.stories.jsx create mode 100644 stories/core/TextInput.stories.jsx create mode 100644 stories/core/UploadInput.stories.jsx create mode 100644 stories/core/UploadInputV2.stories.jsx create mode 100644 stories/core/UploadInputV3.stories.jsx create mode 100644 stories/core/VideoStream.stories.jsx create mode 100644 wrangler.jsonc diff --git a/.gitignore b/.gitignore index c3abc985..75ec2c35 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ package.json.lock docs package-lock.json .claude + +storybook-static +dist diff --git a/.npmignore b/.npmignore index 65fad886..75cbeb7c 100644 --- a/.npmignore +++ b/.npmignore @@ -8,3 +8,7 @@ webpack.prod.js .nvmrc babel.config.json .editorconfig +stories +.storybook +wrangler.jsonc +dist diff --git a/.storybook/main.js b/.storybook/main.js new file mode 100644 index 00000000..348be07d --- /dev/null +++ b/.storybook/main.js @@ -0,0 +1,26 @@ +const styleRule = (test, extraLoader, { modules = false, exclude } = {}) => ({ + test, + ...(exclude ? { exclude } : {}), + use: [ + "style-loader", + { loader: "css-loader", options: { modules, sourceMap: false } }, + ...(extraLoader ? [{ loader: extraLoader, options: { sourceMap: false } }] : []) + ] +}); + +module.exports = { + framework: "@storybook/react-webpack5", + // the webpack5 builder ships no JS compiler; without this addon nothing transpiles JSX + addons: ["@storybook/addon-docs", "@storybook/addon-webpack5-compiler-babel"], + stories: ["../stories/**/*.stories.@(js|jsx)"], + webpackFinal: (config) => { + // the builder ships plain css only; src imports less/scss in both module and global form + config.module.rules.push( + styleRule(/\.module\.less$/, "less-loader", { modules: true }), + styleRule(/\.module\.scss$/, "sass-loader", { modules: true }), + styleRule(/\.less$/, "less-loader", { exclude: /\.module\.less$/ }), + styleRule(/\.scss$/, "sass-loader", { exclude: /\.module\.scss$/ }) + ); + return config; + } +}; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 00000000..68ad7c91 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,10 @@ + + diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx new file mode 100644 index 00000000..bf2ccbfc --- /dev/null +++ b/.storybook/preview.jsx @@ -0,0 +1,44 @@ +import { Provider } from "react-redux"; +import { + legacy_createStore as createStore, + combineReducers, + applyMiddleware +} from "redux"; +import thunk from "redux-thunk"; +import { ThemeProvider, createTheme } from "@mui/material"; +// Legacy (non-MUI) components render `fa fa-*` icons and bootstrap classes. The +// consuming apps supply both: summit-admin imports this same font-awesome css in +// src/index.js, and links bootstrap 3.3.7 from its index.ejs — mirrored for the +// preview iframe in .storybook/preview-head.html. +import "font-awesome/css/font-awesome.css"; +import "../src/i18n/i18n"; // side-effect: T.setTexts, else components render raw keys +import { genericReducers } from "../src/utils/reducers"; +import allFiltersReducer from "../src/components/mui/GridFilter/reducers/all-filters-reducer"; +import { MuiBaseCustomTheme } from "../src/components/mui/MuiBaseCustomTheme"; + +const theme = createTheme(MuiBaseCustomTheme); + +// GridFilter reads allGridFiltersState; SnackbarNotification reads baseState and +// dispatches clearSnackbarMessage, which is a thunk — hence the middleware. +export const store = createStore( + combineReducers({ + baseState: genericReducers, + allGridFiltersState: allFiltersReducer + }), + applyMiddleware(thunk) +); + +export const decorators = [ + (Story) => ( + + + + + + ) +]; + +export const parameters = { controls: { expanded: true } }; + +// gives every component a generated Docs page (props table + live controls) +export const tags = ["autodocs"]; diff --git a/package.json b/package.json index 0932c260..50a6363e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "clean": "rm -Rf node_modules & rm -Rf lib & yarn install", "build-dev": "./node_modules/.bin/webpack --config webpack.dev.js", "build": "./node_modules/.bin/webpack --config webpack.prod.js", - "test": "jest" + "test": "jest", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" }, "license": "APACHE 2.0", "dependencies": { @@ -34,6 +36,9 @@ "@react-pdf/renderer": "^4.4.1", "@sentry/react": "^8.54.0", "@sentry/webpack-plugin": "^3.1.2", + "@storybook/addon-docs": "^10", + "@storybook/addon-webpack5-compiler-babel": "^4", + "@storybook/react-webpack5": "^10", "@stripe/react-stripe-js": "^5.4.1", "@stripe/stripe-js": "^8.5.3", "@testing-library/jest-dom": "5.17.0", @@ -99,6 +104,7 @@ "sass": "^1.77.0", "sass-loader": "^14.2.1", "spark-md5": "^3.0.2", + "storybook": "^10", "style-loader": "^3.3.1", "superagent": "8.0.9", "sweetalert2": "^8.15.2", diff --git a/stories/_helpers.jsx b/stories/_helpers.jsx new file mode 100644 index 00000000..50a5755f --- /dev/null +++ b/stories/_helpers.jsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Formik, Form } from "formik"; + +/** + * Formik-bound inputs read from context, so every formik-inputs story needs a + * host form. Usage: decorators: [withFormik({ myField: "value" })] + */ +export const withFormik = + (initialValues = {}, formProps = {}) => + (Story) => ( + {}} {...formProps}> +
+ + +
+ ); + +/** Table rows have to live inside a table to render at all. */ +export const inTable = (Story) => ( + + + + +
+); + +/** Components that fetch on mount have no backend here — see docs note on each. */ +export const NEEDS_API = + "Fetches on mount via utils/query-actions. With no API reachable from Storybook it renders its empty state; wire msw to populate it."; + +export const sampleRows = [ + { id: 1, name: "Diamond Sponsorship", quantity: 2, price: 25000 }, + { id: 2, name: "Booth 10x10", quantity: 1, price: 8000 }, + { id: 3, name: "Lanyard Branding", quantity: 4, price: 1500 } +]; + +export const sampleColumns = [ + { columnKey: "name", header: "Item", sortable: true }, + { columnKey: "quantity", header: "Qty", align: "right" }, + { columnKey: "price", header: "Price", align: "right" } +]; diff --git a/stories/core/ActionDropdown.stories.jsx b/stories/core/ActionDropdown.stories.jsx new file mode 100644 index 00000000..2bc7fc9d --- /dev/null +++ b/stories/core/ActionDropdown.stories.jsx @@ -0,0 +1,16 @@ +import ActionDropdown from "../../src/components/inputs/action-dropdown"; + +export default { + title: "Core/Inputs/ActionDropdown", + component: ActionDropdown, + argTypes: { onClick: { action: "action" } } +}; + +const options = [ + { value: "export_csv", label: "Export CSV" }, + { value: "send_email", label: "Send Email" }, + { value: "archive", label: "Archive" } +]; + +export const Default = { args: { options, actionLabel: "Go", placeholder: "Bulk action..." } }; +export const Small = { args: { ...Default.args, small: true } }; diff --git a/stories/core/AjaxLoader.stories.jsx b/stories/core/AjaxLoader.stories.jsx new file mode 100644 index 00000000..5527fc71 --- /dev/null +++ b/stories/core/AjaxLoader.stories.jsx @@ -0,0 +1,17 @@ +import AjaxLoader from "../../src/components/ajaxloader"; + +export default { + title: "Core/Feedback/AjaxLoader", + component: AjaxLoader, + // fixed-position overlay; relative + a positioned box keeps it inside the story frame + decorators: [ + (Story) => ( +
+ +
+ ) + ] +}; + +export const Default = { args: { show: true, relative: true, size: 40 } }; +export const Hidden = { args: { show: false, relative: true, size: 40 } }; diff --git a/stories/core/AsyncEntityInputs.stories.jsx b/stories/core/AsyncEntityInputs.stories.jsx new file mode 100644 index 00000000..fd1fb311 --- /dev/null +++ b/stories/core/AsyncEntityInputs.stories.jsx @@ -0,0 +1,59 @@ +import CompanyInput from "../../src/components/inputs/company-input"; +import PromocodeInput from "../../src/components/inputs/promocode-input"; +import SponsorInput from "../../src/components/inputs/sponsor-input"; +import OrganizationInput from "../../src/components/inputs/organization-input"; +import EventInput from "../../src/components/inputs/event-input"; +import GroupInput from "../../src/components/inputs/group-input"; +import MemberInput from "../../src/components/inputs/member-input"; +import AttendeeInput from "../../src/components/inputs/attendee-input"; +import SummitInput from "../../src/components/inputs/summit-input"; +import SpeakerInput from "../../src/components/inputs/speaker-input"; +import OperatorInput from "../../src/components/inputs/operator-input"; +import TagInput from "../../src/components/inputs/tag-input"; +import AccessLevelsInput from "../../src/components/inputs/access-levels-input"; +import RegistrationCompanyInput from "../../src/components/inputs/registration-company-input"; +import TicketTypesInput from "../../src/components/inputs/ticket-types-input.js"; +import SponsoredProjectInput from "../../src/components/inputs/sponsored-project-input.js"; +import CountryInput from "../../src/components/inputs/country-input"; +import LanguageInput from "../../src/components/inputs/language-input"; +import CountryDropdown from "../../src/components/inputs/country-dropdown"; +import { NEEDS_API } from "../_helpers"; + +/** + * Every input here is an async select over utils/query-actions — one story each + * would be 19 near-identical files, so they share this one. NEEDS_API applies + * to all of them: they render and open, but option lists stay empty offline. + */ +export default { + title: "Core/Inputs/AsyncEntityInputs", + parameters: { docs: { description: { component: NEEDS_API } } }, + argTypes: { onChange: { action: "changed" } } +}; + +const base = { value: null, placeholder: "Type to search..." }; + +export const Company = { render: (args) => }; +export const Promocode = { render: (args) => }; +export const Sponsor = { render: (args) =>