diff --git a/.github/workflows/storybook.yaml b/.github/workflows/storybook.yaml new file mode 100644 index 000000000..bed579eb3 --- /dev/null +++ b/.github/workflows/storybook.yaml @@ -0,0 +1,34 @@ +name: Storybook + +on: + push: + branches: + - dev + - main + pull_request: + paths: + - ".github/workflows/storybook.yaml" + - "packages/popcorntime-ui/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + chromatic-ui: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - uses: ./.github/actions/init-env-node + - name: Run Chromatic + uses: chromaui/action@latest + with: + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + zip: true + onlyChanged: true + skip: "dependabot/**" + workingDir: packages/popcorntime-ui + autoAcceptChanges: false + exitOnceUploaded: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index fb7c58f3a..916eae505 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,6 @@ dist-ssr *.sw? .turbo .env -.vite \ No newline at end of file +.vite +*storybook.log +storybook-static diff --git a/biome.json b/biome.json index 2a2a782eb..0af1973d3 100644 --- a/biome.json +++ b/biome.json @@ -2,7 +2,14 @@ "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json", "files": { "ignoreUnknown": false, - "includes": ["apps/**", "!**/dist", "!**/tauri/types.ts"] + "includes": [ + "apps/**", + "packages/**", + "!**/dist", + "!**/tauri/types.ts", + "!packages/translator/*", + "!**/*.stories.tsx" + ] }, "formatter": { "enabled": true, diff --git a/packages/popcorntime-i18n/locales.ts b/packages/popcorntime-i18n/locales.ts index 9404c5905..e183f7d12 100644 --- a/packages/popcorntime-i18n/locales.ts +++ b/packages/popcorntime-i18n/locales.ts @@ -1,138 +1,138 @@ export default { - AE: { - default: "ar", - languages: ["ar", "fa", "ur", "hi"], - }, - US: { - default: "en", - languages: ["en"], - }, - DE: { - default: "de", - languages: ["de"], - }, - GB: { - default: "en", - languages: ["en"], - }, - CA: { - default: "en", - languages: ["en", "fr"], - }, - FR: { - default: "fr", - languages: ["fr"], - }, - BE: { - default: "fr", - languages: ["fr", "nl", "de"], - }, - AT: { - default: "de", - languages: ["de"], - }, - AU: { - default: "en", - languages: ["en"], - }, - SE: { - default: "sv", - languages: ["sv"], - }, - IE: { - default: "en", - languages: ["en", "ga"], - }, - FI: { - default: "fi", - languages: ["fi", "sv"], - }, - NL: { - default: "nl", - languages: ["nl"], - }, - IL: { - default: "he", - languages: ["he", "ar"], - }, - NO: { - default: "no", - languages: ["no"], - }, - DK: { - default: "da", - languages: ["da"], - }, - CH: { - default: "de", - languages: ["de", "fr", "it"], - }, - IT: { - default: "it", - languages: ["it"], - }, - ES: { - default: "es", - languages: ["es"], - }, - JP: { - default: "ja", - languages: ["ja"], - }, - PL: { - default: "pl", - languages: ["pl"], - }, - MX: { - default: "es", - languages: ["es"], - }, - IN: { - default: "hi", - languages: ["hi", "en"], - }, - TR: { - default: "tr", - languages: ["tr"], - }, - NZ: { - default: "en", - languages: ["en"], - }, - DZ: { - default: "ar", - languages: ["ar", "fr"], - }, - EE: { - default: "et", - languages: ["et"], - }, - EG: { - default: "ar", - languages: ["ar"], - }, - GR: { - default: "el", - languages: ["el"], - }, - PT: { - default: "pt", - languages: ["pt"], - }, - BR: { - default: "pt", - languages: ["pt"], - }, - RS: { - default: "sr", - languages: ["sr"], - }, - SA: { - default: "ar", - languages: ["ar"], - }, - MA: { - default: "ar", - languages: ["ar", "fr"], - }, -} as const; \ No newline at end of file + AE: { + default: "ar", + languages: ["ar", "fa", "ur", "hi"], + }, + US: { + default: "en", + languages: ["en"], + }, + DE: { + default: "de", + languages: ["de"], + }, + GB: { + default: "en", + languages: ["en"], + }, + CA: { + default: "en", + languages: ["en", "fr"], + }, + FR: { + default: "fr", + languages: ["fr"], + }, + BE: { + default: "fr", + languages: ["fr", "nl", "de"], + }, + AT: { + default: "de", + languages: ["de"], + }, + AU: { + default: "en", + languages: ["en"], + }, + SE: { + default: "sv", + languages: ["sv"], + }, + IE: { + default: "en", + languages: ["en", "ga"], + }, + FI: { + default: "fi", + languages: ["fi", "sv"], + }, + NL: { + default: "nl", + languages: ["nl"], + }, + IL: { + default: "he", + languages: ["he", "ar"], + }, + NO: { + default: "no", + languages: ["no"], + }, + DK: { + default: "da", + languages: ["da"], + }, + CH: { + default: "de", + languages: ["de", "fr", "it"], + }, + IT: { + default: "it", + languages: ["it"], + }, + ES: { + default: "es", + languages: ["es"], + }, + JP: { + default: "ja", + languages: ["ja"], + }, + PL: { + default: "pl", + languages: ["pl"], + }, + MX: { + default: "es", + languages: ["es"], + }, + IN: { + default: "hi", + languages: ["hi", "en"], + }, + TR: { + default: "tr", + languages: ["tr"], + }, + NZ: { + default: "en", + languages: ["en"], + }, + DZ: { + default: "ar", + languages: ["ar", "fr"], + }, + EE: { + default: "et", + languages: ["et"], + }, + EG: { + default: "ar", + languages: ["ar"], + }, + GR: { + default: "el", + languages: ["el"], + }, + PT: { + default: "pt", + languages: ["pt"], + }, + BR: { + default: "pt", + languages: ["pt"], + }, + RS: { + default: "sr", + languages: ["sr"], + }, + SA: { + default: "ar", + languages: ["ar"], + }, + MA: { + default: "ar", + languages: ["ar", "fr"], + }, +} as const; diff --git a/packages/popcorntime-i18n/package.json b/packages/popcorntime-i18n/package.json index 8ef0c2572..6788531eb 100644 --- a/packages/popcorntime-i18n/package.json +++ b/packages/popcorntime-i18n/package.json @@ -1,17 +1,17 @@ { - "name": "@popcorntime/i18n", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "src/index.ts", - "exports": { - ".": "./src/index.ts", - "./types": "./src/types.ts" - }, - "scripts": {}, - "devDependencies": { - "@popcorntime/typescript-config": "workspace:*", - "typescript": "catalog:" - }, - "dependencies": {} + "name": "@popcorntime/i18n", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./types": "./src/types.ts" + }, + "scripts": {}, + "devDependencies": { + "@popcorntime/typescript-config": "workspace:*", + "typescript": "catalog:" + }, + "dependencies": {} } diff --git a/packages/popcorntime-i18n/src/index.ts b/packages/popcorntime-i18n/src/index.ts index 4160f4f54..b041074ea 100644 --- a/packages/popcorntime-i18n/src/index.ts +++ b/packages/popcorntime-i18n/src/index.ts @@ -1,42 +1,43 @@ import { - i18n, - Locale, - Country, - countryVariations, - DefaultLocaleTag, - LocaleTag, - CountryPath, - countries, + type Country, + type CountryPath, + countries, + countryVariations, + type DefaultLocaleTag, + i18n, + type Locale, + type LocaleTag, } from "./types.js"; + export { i18n, locales } from "./types.js"; export type { Locale, Country, DefaultLocaleTag, LocaleTag, CountryPath }; export { countries }; export type LocaleCookie = { - locale: Locale; - forced: boolean; + locale: Locale; + forced: boolean; }; export function getCountryLocale(country: Country): Locale { - return i18n.raw[country].default; + return i18n.raw[country].default; } export function isDefaultLocale(country: Country, userLocale: Locale) { - return getCountryLocale(country) === userLocale; + return getCountryLocale(country) === userLocale; } export function getLocalesForCountry(country: Country) { - return i18n.raw[country].languages; + return i18n.raw[country].languages; } export function getVariationForLocale(country: Country, userLocale: Locale) { - if (isDefaultLocale(country, userLocale)) { - if (countryVariations[country]) { - return countryVariations[country]; - } else { - return 1; - } - } else { - // return default variation - return 1; - } + if (isDefaultLocale(country, userLocale)) { + if (countryVariations[country]) { + return countryVariations[country]; + } else { + return 1; + } + } else { + // return default variation + return 1; + } } diff --git a/packages/popcorntime-i18n/src/types.ts b/packages/popcorntime-i18n/src/types.ts index d7df8ed9c..ae685bdda 100644 --- a/packages/popcorntime-i18n/src/types.ts +++ b/packages/popcorntime-i18n/src/types.ts @@ -4,82 +4,80 @@ export type Country = keyof typeof rawLocales; export type Locale = RawLocales["languages"][number]; export type Variation = 1 | 2 | 3 | 4 | 5; export type LocaleTag = { - [C in Country]: `${(typeof rawLocales)[C]["languages"][number]}-${Uppercase}`; + [C in Country]: `${(typeof rawLocales)[C]["languages"][number]}-${Uppercase}`; }[Country]; export type DefaultLocaleTag = { - [C in Country]: `${(typeof rawLocales)[C]["default"]}-${Uppercase}`; + [C in Country]: `${(typeof rawLocales)[C]["default"]}-${Uppercase}`; }[Country]; export type CountryPath = { - [C in Country]: - | C - | Exclude< - `${C}-${(typeof rawLocales)[C]["languages"][number]}`, - `${C}-${(typeof rawLocales)[C]["default"]}` - >; + [C in Country]: + | C + | Exclude< + `${C}-${(typeof rawLocales)[C]["languages"][number]}`, + `${C}-${(typeof rawLocales)[C]["default"]}` + >; }[Country]; function buildCountryVariations() { - const result: Partial> = {}; - const localeVariation: Partial> = {}; + const result: Partial> = {}; + const localeVariation: Partial> = {}; - for (const country of Object.keys(rawLocales) as Country[]) { - const defaultLocale = rawLocales[country].default; - const order = localeVariation[defaultLocale] || 1; + for (const country of Object.keys(rawLocales) as Country[]) { + const defaultLocale = rawLocales[country].default; + const order = localeVariation[defaultLocale] || 1; - result[country] = order as Variation; + result[country] = order as Variation; - if (order < 5) { - localeVariation[defaultLocale] = (order + 1) as Variation; - } else { - localeVariation[defaultLocale] = 1; - } - } + if (order < 5) { + localeVariation[defaultLocale] = (order + 1) as Variation; + } else { + localeVariation[defaultLocale] = 1; + } + } - return result as Record; + return result as Record; } export const countryVariations = buildCountryVariations(); export const locales = Array.from( - new Set(Object.values(rawLocales).flatMap((data) => data.languages)) + new Set(Object.values(rawLocales).flatMap(data => data.languages)) ) as Locale[]; export const localeTags = Array.from( - new Set( - Object.entries(rawLocales).flatMap(([country, data]) => - data.languages.map((lang) => `${lang}-${country.toUpperCase()}`) - ) - ) + new Set( + Object.entries(rawLocales).flatMap(([country, data]) => + data.languages.map(lang => `${lang}-${country.toUpperCase()}`) + ) + ) ) as LocaleTag[]; export const defaultLocaleTags = Array.from( - new Set( - Object.entries(rawLocales).flatMap( - ([country, data]) => `${data.default}-${country.toUpperCase()}` - ) - ) + new Set( + Object.entries(rawLocales).flatMap( + ([country, data]) => `${data.default}-${country.toUpperCase()}` + ) + ) ) as DefaultLocaleTag[]; export const countryPaths = Array.from( - new Set( - Object.entries(rawLocales).flatMap(([country, data]) => [ - country, - ...data.languages - .filter((lang) => lang !== data.default) - .map((lang) => `${country}-${lang}`), - ]) - ) + new Set( + Object.entries(rawLocales).flatMap(([country, data]) => [ + country, + ...data.languages.filter(lang => lang !== data.default).map(lang => `${country}-${lang}`), + ]) + ) ) as CountryPath[]; export const countries = Object.keys(rawLocales) as Country[]; export const i18n = { - defaultLocale: "en", - defaultCountry: "US", - locales, - countries, - raw: rawLocales, - localeTags, - defaultLocaleTags, - countryPaths, + defaultLocale: "en", + defaultCountry: "US", + locales, + countries, + raw: rawLocales, + localeTags, + defaultLocaleTags, + countryPaths, } as const; diff --git a/packages/popcorntime-i18n/tsconfig.json b/packages/popcorntime-i18n/tsconfig.json index 24d89541d..d32fc721a 100644 --- a/packages/popcorntime-i18n/tsconfig.json +++ b/packages/popcorntime-i18n/tsconfig.json @@ -1,11 +1,11 @@ { - "extends": "@popcorntime/typescript-config/base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@popcorntime/i18n/*": ["./src/*"] - } - }, - "include": ["."], - "exclude": ["node_modules", "dist"] + "extends": "@popcorntime/typescript-config/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@popcorntime/i18n/*": ["./src/*"] + } + }, + "include": ["."], + "exclude": ["node_modules", "dist"] } diff --git a/packages/popcorntime-ui/.storybook/main.ts b/packages/popcorntime-ui/.storybook/main.ts new file mode 100644 index 000000000..2edcc748a --- /dev/null +++ b/packages/popcorntime-ui/.storybook/main.ts @@ -0,0 +1,25 @@ +import { dirname, join } from "node:path"; +import type { StorybookConfig } from "@storybook/react-vite"; + +function getAbsolutePath(value: string) { + return dirname(require.resolve(join(value, "package.json"))); +} +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + + addons: [ + getAbsolutePath("@storybook/addon-themes"), + getAbsolutePath("@storybook/addon-a11y"), + getAbsolutePath("@storybook/addon-docs"), + ], + + framework: { + name: getAbsolutePath("@storybook/react-vite"), + options: {}, + }, + + typescript: { + check: true, + }, +}; +export default config; diff --git a/packages/popcorntime-ui/.storybook/preview.tsx b/packages/popcorntime-ui/.storybook/preview.tsx new file mode 100644 index 000000000..7d48a2710 --- /dev/null +++ b/packages/popcorntime-ui/.storybook/preview.tsx @@ -0,0 +1,33 @@ +import { withThemeByDataAttribute } from "@storybook/addon-themes"; +import type { Preview } from "@storybook/react-vite"; +import "../src/styles/globals.css"; + +export const decorators = [ + withThemeByDataAttribute({ + themes: { + light: "light", + dark: "dark", + }, + defaultTheme: "light", + attributeName: "data-theme", + }), +]; + +const preview: Preview = { + parameters: { + docs: { + story: { inline: false }, + inlineStories: false, + }, + a11y: { + context: "#storybook-root", + config: {}, + options: {}, + manual: false, + }, + }, + + tags: ["autodocs"], +}; + +export default preview; diff --git a/packages/popcorntime-ui/STORYBOOK.md b/packages/popcorntime-ui/STORYBOOK.md new file mode 100644 index 000000000..defd0fed4 --- /dev/null +++ b/packages/popcorntime-ui/STORYBOOK.md @@ -0,0 +1,35 @@ +# Storybook Documentation + +This project uses [Storybook](https://storybook.js.org/) to document, develop, and test the UI components in `@popcorntime/ui`. + +## Getting Started + +### Local Development + +To run Storybook locally: + +```bash +# From the project root +pnpm --filter @popcorntime/ui storybook + +# Or from the UI package directory +cd packages/popcorntime-ui +pnpm storybook +``` + +This will start Storybook on `http://localhost:6006`. + +### Building for Production + +To build a static version of Storybook: + +```bash +# From the project root +pnpm --filter @popcorntime/ui build-storybook + +# Or from the UI package directory +cd packages/popcorntime-ui +pnpm build-storybook +``` + +The static build will be created in `packages/popcorntime-ui/storybook-static/`. diff --git a/packages/popcorntime-ui/components.json b/packages/popcorntime-ui/components.json index 98744c552..da145cc16 100644 --- a/packages/popcorntime-ui/components.json +++ b/packages/popcorntime-ui/components.json @@ -1,20 +1,20 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": true, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/styles/globals.css", - "baseColor": "zinc", - "cssVariables": true - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@popcorntime/ui/components", - "utils": "@popcorntime/ui/lib/utils", - "hooks": "@popcorntime/ui/hooks", - "lib": "@popcorntime/ui/lib", - "ui": "@popcorntime/ui/components" - } + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "zinc", + "cssVariables": true + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@popcorntime/ui/components", + "utils": "@popcorntime/ui/lib/utils", + "hooks": "@popcorntime/ui/hooks", + "lib": "@popcorntime/ui/lib", + "ui": "@popcorntime/ui/components" + } } diff --git a/packages/popcorntime-ui/package.json b/packages/popcorntime-ui/package.json index 987e70a14..693b47e36 100644 --- a/packages/popcorntime-ui/package.json +++ b/packages/popcorntime-ui/package.json @@ -1,63 +1,73 @@ { - "name": "@popcorntime/ui", - "private": true, - "version": "0.0.0", - "type": "module", - "exports": { - "./styles.css": "./src/styles/globals.css", - "./postcss.config": "./postcss.config.mjs", - "./lib/*": "./src/lib/*.ts", - "./blocks/*": "./src/blocks/*.tsx", - "./components/*": "./src/components/*.tsx", - "./hooks/*": "./src/hooks/*.ts" - }, - "scripts": { - "type-check": "tsc --noEmit" - }, - "devDependencies": { - "@popcorntime/typescript-config": "workspace:*", - "@tailwindcss/postcss": "^4.1.13", - "@types/react": "^19.1.13", - "@types/react-dom": "^19.1.9", - "@vitejs/plugin-react-swc": "4.1.0", - "autoprefixer": "^10.4.20", - "globals": "^16.4.0", - "postcss": "^8.5.3", - "tailwind-merge": "^3.3.1", - "typescript": "^5.9.2", - "vite": "^6.1.0" - }, - "dependencies": { - "@hookform/resolvers": "catalog:", - "@tailwindcss/vite": "catalog:", - "lucide-react": "catalog:", - "react": "catalog:", - "react-dom": "catalog:", - "react-hook-form": "catalog:", - "sonner": "catalog:", - "tailwindcss": "catalog:", - "zod": "catalog:", - "@radix-ui/react-alert-dialog": "^1.1.15", - "@radix-ui/react-avatar": "^1.1.10", - "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-label": "^2.1.7", - "@radix-ui/react-menubar": "^1.1.16", - "@radix-ui/react-navigation-menu": "^1.2.14", - "@radix-ui/react-popover": "^1.1.15", - "@radix-ui/react-scroll-area": "^1.2.10", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tabs": "^1.1.13", - "@radix-ui/react-toggle": "^1.1.10", - "@radix-ui/react-toggle-group": "^1.1.11", - "@radix-ui/react-tooltip": "^1.2.8", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "framer-motion": "^12.23.21", - "next-themes": "^0.4.6" - } + "name": "@popcorntime/ui", + "private": true, + "version": "0.0.0", + "type": "module", + "exports": { + "./styles.css": "./src/styles/globals.css", + "./postcss.config": "./postcss.config.mjs", + "./lib/*": "./src/lib/*.ts", + "./blocks/*": "./src/blocks/*.tsx", + "./components/*": "./src/components/*.tsx", + "./hooks/*": "./src/hooks/*.ts" + }, + "scripts": { + "type-check": "tsc --noEmit", + "storybook": "storybook dev -p 6006", + "chromatic": "chromatic --exit-zero-on-changes", + "build-storybook": "storybook build" + }, + "devDependencies": { + "@popcorntime/typescript-config": "workspace:*", + "@storybook/addon-a11y": "^9.1.8", + "@storybook/addon-docs": "^9.1.8", + "@storybook/addon-themes": "^9.1.8", + "@storybook/react-vite": "^9.1.8", + "@storybook/test-runner": "^0.23.0", + "@tailwindcss/postcss": "^4.1.13", + "@types/react": "^19.1.13", + "@types/react-dom": "^19.1.9", + "@vitejs/plugin-react-swc": "4.1.0", + "autoprefixer": "^10.4.20", + "chromatic": "^13.2.1", + "globals": "^16.4.0", + "postcss": "^8.5.3", + "storybook": "^9.1.8", + "tailwind-merge": "^3.3.1", + "typescript": "^5.9.2", + "vite": "^6.1.0" + }, + "dependencies": { + "@hookform/resolvers": "catalog:", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.16", + "@radix-ui/react-navigation-menu": "^1.2.14", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-toggle-group": "^1.1.11", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "catalog:", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "framer-motion": "^12.23.21", + "lucide-react": "catalog:", + "next-themes": "^0.4.6", + "react": "catalog:", + "react-dom": "catalog:", + "react-hook-form": "catalog:", + "sonner": "catalog:", + "tailwindcss": "catalog:", + "zod": "catalog:" + } } diff --git a/packages/popcorntime-ui/postcss.config.mjs b/packages/popcorntime-ui/postcss.config.mjs index 4ae682d87..143af71cc 100644 --- a/packages/popcorntime-ui/postcss.config.mjs +++ b/packages/popcorntime-ui/postcss.config.mjs @@ -1,6 +1,6 @@ /** @type {import('postcss-load-config').Config} */ const config = { - plugins: { "@tailwindcss/postcss": {} }, + plugins: { "@tailwindcss/postcss": {} }, }; export default config; diff --git a/packages/popcorntime-ui/src/Welcome.stories.tsx b/packages/popcorntime-ui/src/Welcome.stories.tsx new file mode 100644 index 000000000..5d449a694 --- /dev/null +++ b/packages/popcorntime-ui/src/Welcome.stories.tsx @@ -0,0 +1,150 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +const meta = { + title: "Welcome", + parameters: { + layout: "centered", + docs: { + page: () => ( +
+

@popcorntime/ui

+ +
+

+ Welcome to the Popcorn Time UI component library documentation. + This Storybook contains interactive examples and documentation for + all our reusable UI components. +

+ +

+ 📚 What you'll find here +

+
    +
  • + Component Examples - Interactive stories + showing components in different states +
  • +
  • + Documentation - Comprehensive guides and prop + tables +
  • +
  • + Accessibility Testing - Built-in a11y checks + for inclusive design +
  • +
  • + Design Guidelines - Best practices and usage + patterns +
  • +
+ +

+ 🎨 Component Coverage +

+
+
+

Form Components

+
    +
  • + Button - Primary, secondary, and specialized actions +
  • +
  • Input - Text fields with validation states
  • +
  • Checkbox - Selection controls
  • +
  • Label - Form element labels
  • +
  • Toggle - Switch controls
  • +
+
+
+

Display Components

+
    +
  • Avatar - User profile images
  • +
  • Badge - Status and category indicators
  • +
  • Separator - Visual content dividers
  • +
  • Tooltip - Contextual help and information
  • +
+
+
+

Layout Components

+
    +
  • Dialog - Modal and overlay content
  • +
  • Tabs - Content organization
  • +
+
+
+ +

🚀 Getting Started

+
+

+ To use these components in your project: +

+
+                {`import { Button, Input } from '@popcorntime/ui/components/button'
+import { Dialog, DialogContent } from '@popcorntime/ui/components/dialog'
+
+function MyComponent() {
+  return (
+    
+ + +
+ ) +}`}
+
+
+ +

✨ Features

+
+
+
🎛️
+

Interactive Controls

+

+ Modify props in real-time +

+
+
+
+

Accessibility

+

+ Built-in a11y testing +

+
+
+
📱
+

Responsive

+

+ Mobile-first design +

+
+
+
+
+ ), + }, + }, + tags: ["autodocs"], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Welcome: Story = { + render: () => ( +
+

Welcome to PopcornTime UI

+

+ Explore our component library using the sidebar navigation +

+
+ + 11 Components + + + Fully Accessible + + + TypeScript + +
+
+ ), +}; diff --git a/packages/popcorntime-ui/src/components/alert-dialog.stories.tsx b/packages/popcorntime-ui/src/components/alert-dialog.stories.tsx new file mode 100644 index 000000000..f16763e8a --- /dev/null +++ b/packages/popcorntime-ui/src/components/alert-dialog.stories.tsx @@ -0,0 +1,336 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@popcorntime/ui/components/alert-dialog"; +import { Button } from "@popcorntime/ui/components/button.js"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { fn } from "storybook/test"; + +const meta = { + title: "Components/AlertDialog", + component: AlertDialog, + parameters: { + layout: "centered", + docs: { + description: { + component: + "A modal dialog that interrupts the user with important content and expects a response.", + }, + }, + }, + tags: ["autodocs"], + argTypes: { + open: { + control: "boolean", + description: "Whether the alert dialog is open", + }, + onOpenChange: { + action: "onOpenChange", + description: "Callback when dialog open state changes", + }, + }, + args: { + onOpenChange: fn(), + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + + + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your account and remove your + data from our servers. + + + + Cancel + Continue + + + + ), +}; + +export const Destructive: Story = { + render: () => ( + + + + + + + Delete Account + + This will permanently delete your PopcornTime account and all associated data. You will + lose access to your watchlist, favorites, and viewing history. + + + + Keep Account + + Delete Account + + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog for destructive actions with appropriate styling", + }, + }, + }, +}; + +export const RemoveFromWatchlist: Story = { + render: () => ( + + + + + + + Remove from Watchlist? + + "Inception" will be removed from your watchlist. You can add it back anytime from the + movie details page. + + + + Keep in Watchlist + Remove + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog for removing items from watchlist", + }, + }, + }, +}; + +export const ClearDownloads: Story = { + render: () => ( + + + + + + + Clear All Downloads? + + This will delete all downloaded movies and TV show episodes from your device. This will + free up storage space but you'll need to download them again to watch offline. + + + + Cancel + + Clear Downloads + + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog for clearing downloaded content", + }, + }, + }, +}; + +export const SignOut: Story = { + render: () => ( + + + + + + + Sign out of PopcornTime? + + You'll need to sign in again to access your personalized content, watchlist, and + preferences. + + + + Stay Signed In + Sign Out + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog for signing out of the application", + }, + }, + }, +}; + +export const UnsavedChanges: Story = { + render: () => ( + + + + + + + Unsaved Changes + + You have unsaved changes to your preferences. If you close now, your changes will be + lost. + + + + Continue Editing + + Discard Changes + + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog warning about unsaved changes", + }, + }, + }, +}; + +export const NetworkError: Story = { + render: () => ( + + + + + + + Connection Failed + + Unable to connect to PopcornTime servers. Please check your internet connection and try + again. If the problem persists, the service may be temporarily unavailable. + + + + Go Offline + Retry Connection + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog for network connectivity issues", + }, + }, + }, +}; + +export const SingleAction: Story = { + render: () => ( + + + + + + + Update Available + + PopcornTime 2.1.0 is now available! This update includes performance improvements, bug + fixes, and new features. The update will be installed automatically when you restart the + application. + + + + Got it + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog with single action button for informational messages", + }, + }, + }, +}; + +export const CustomStyling: Story = { + render: () => ( + + + + + + + + 🎬 Premium Features + + + Upgrade to PopcornTime Premium to unlock exclusive features like 4K streaming, ad-free + experience, offline downloads, and access to premium content libraries. + + +
+
+
+
+ 4K Ultra HD Streaming +
+
+
+ No Advertisements +
+
+
+ Unlimited Downloads +
+
+
+ + Maybe Later + + Upgrade Now + + + + + ), + parameters: { + docs: { + description: { + story: "Alert dialog with custom styling and content layout", + }, + }, + }, +}; diff --git a/packages/popcorntime-ui/src/components/alert-dialog.tsx b/packages/popcorntime-ui/src/components/alert-dialog.tsx index e551a5970..d6421e690 100644 --- a/packages/popcorntime-ui/src/components/alert-dialog.tsx +++ b/packages/popcorntime-ui/src/components/alert-dialog.tsx @@ -1,157 +1,134 @@ -"use client" +"use client"; -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import { buttonVariants } from "@popcorntime/ui/components/button"; +import { cn } from "@popcorntime/ui/lib/utils"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; +import type * as React from "react"; -import { cn } from "@popcorntime/ui/lib/utils" -import { buttonVariants } from "@popcorntime/ui/components/button" - -function AlertDialog({ - ...props -}: React.ComponentProps) { - return +function AlertDialog({ ...props }: React.ComponentProps) { + return ; } function AlertDialogTrigger({ - ...props + ...props }: React.ComponentProps) { - return ( - - ) + return ; } -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return ; } function AlertDialogOverlay({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogContent({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - - - - ) + return ( + + + + + ); } -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) +function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); } -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) +function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); } function AlertDialogTitle({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogDescription({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } function AlertDialogAction({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ; } function AlertDialogCancel({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/packages/popcorntime-ui/src/components/avatar.stories.tsx b/packages/popcorntime-ui/src/components/avatar.stories.tsx new file mode 100644 index 000000000..458475e19 --- /dev/null +++ b/packages/popcorntime-ui/src/components/avatar.stories.tsx @@ -0,0 +1,204 @@ +import { Avatar, AvatarFallback, AvatarImage } from "@popcorntime/ui/components/avatar"; +import type { Meta, StoryObj } from "@storybook/react-vite"; + +const meta = { + title: "Components/Avatar", + component: Avatar, + parameters: { + layout: "centered", + docs: { + description: { + component: + "An avatar component for displaying user profile pictures with fallback support.", + }, + }, + }, + tags: ["autodocs"], + argTypes: { + className: { + control: "text", + description: "Additional CSS classes", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + CN + + ), +}; + +export const WithFallback: Story = { + render: () => ( + + + JD + + ), + parameters: { + docs: { + description: { + story: "Avatar with a broken image URL, showing the fallback initials", + }, + }, + }, +}; + +export const Sizes: Story = { + render: () => ( +
+ + + CN + + + + CN + + + + CN + + + + CN + + + + CN + +
+ ), + parameters: { + docs: { + description: { + story: "Avatars in different sizes", + }, + }, + }, +}; + +export const FallbackVariations: Story = { + render: () => ( +
+ + JD + + + AB + + + ? + + + + + + + + +
+ ), + parameters: { + docs: { + description: { + story: "Different fallback styles including colored backgrounds and icons", + }, + }, + }, +}; + +export const UserList: Story = { + render: () => { + const users = [ + { + name: "John Doe", + image: + "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=32&h=32&fit=crop&crop=face", + initials: "JD", + }, + { + name: "Jane Smith", + image: + "https://images.unsplash.com/photo-1494790108755-2616b86b5e0b?w=32&h=32&fit=crop&crop=face", + initials: "JS", + }, + { name: "Alice Johnson", image: "/broken.jpg", initials: "AJ" }, + { + name: "Bob Wilson", + image: + "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=32&h=32&fit=crop&crop=face", + initials: "BW", + }, + ]; + + return ( +
+ {users.map(user => ( +
+ + + {user.initials} + + {user.name} +
+ ))} +
+ ); + }, + parameters: { + docs: { + description: { + story: "Avatar component used in a user list with mixed image loading states", + }, + }, + }, +}; + +export const AvatarGroup: Story = { + render: () => ( +
+ + + U1 + + + + U2 + + + + U3 + + + +5 + +
+ ), + parameters: { + docs: { + description: { + story: "Overlapping avatar group showing multiple users with a count indicator", + }, + }, + }, +}; diff --git a/packages/popcorntime-ui/src/components/avatar.tsx b/packages/popcorntime-ui/src/components/avatar.tsx index 4d4ff28a3..6742790b5 100644 --- a/packages/popcorntime-ui/src/components/avatar.tsx +++ b/packages/popcorntime-ui/src/components/avatar.tsx @@ -1,53 +1,40 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import { cn } from "@popcorntime/ui/lib/utils"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import type * as React from "react"; -import { cn } from "@popcorntime/ui/lib/utils" - -function Avatar({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) +function Avatar({ className, ...props }: React.ComponentProps) { + return ( + + ); } -function AvatarImage({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) +function AvatarImage({ className, ...props }: React.ComponentProps) { + return ( + + ); } function AvatarFallback({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ) + return ( + + ); } -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/packages/popcorntime-ui/src/components/badge.stories.tsx b/packages/popcorntime-ui/src/components/badge.stories.tsx new file mode 100644 index 000000000..74a984b05 --- /dev/null +++ b/packages/popcorntime-ui/src/components/badge.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Badge } from "@popcorntime/ui/components/badge.js"; + +const meta = { + title: "Components/Badge", + component: Badge, + parameters: { + layout: "centered", + docs: { + description: { + component: "A small label component for displaying status, categories, or other metadata.", + }, + }, + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: { type: "select" }, + options: ["default", "secondary", "destructive", "outline"], + description: "The visual variant of the badge", + }, + children: { + control: "text", + description: "Badge content", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + children: "Badge", + }, +}; + +export const Variants: Story = { + render: () => ( +
+ Default + Secondary + Destructive + Outline +
+ ), + parameters: { + docs: { + description: { + story: "All available badge variants", + }, + }, + }, +}; + +export const Status: Story = { + render: () => ( +
+ Active + Pending + Error + Draft +
+ ), + parameters: { + docs: { + description: { + story: "Badges used to indicate different statuses", + }, + }, + }, +}; + +export const WithIcon: Story = { + render: () => ( +
+ + + + + Completed + + + + + + + Failed + + + + + + + Processing + +
+ ), + parameters: { + docs: { + description: { + story: "Badges with icons to provide visual context", + }, + }, + }, +}; + +export const Numbers: Story = { + render: () => ( +
+ 1 + 99+ + 42 + 0 +
+ ), + parameters: { + docs: { + description: { + story: "Badges displaying numbers or counts", + }, + }, + }, +}; + +export const Categories: Story = { + render: () => ( +
+ React + TypeScript + Tailwind CSS + Storybook + Accessibility + Design System +
+ ), + parameters: { + docs: { + description: { + story: "Badges used as category tags or labels", + }, + }, + }, +}; + +export const Sizes: Story = { + render: () => ( +
+ Small + Medium + Large +
+ ), + parameters: { + docs: { + description: { + story: "Custom badge sizes using className overrides", + }, + }, + }, +}; diff --git a/packages/popcorntime-ui/src/components/badge.tsx b/packages/popcorntime-ui/src/components/badge.tsx index b5a2847d8..bb7ba4a9e 100644 --- a/packages/popcorntime-ui/src/components/badge.tsx +++ b/packages/popcorntime-ui/src/components/badge.tsx @@ -1,35 +1,32 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@popcorntime/ui/lib/utils"; +import { cva, type VariantProps } from "class-variance-authority"; +import type * as React from "react"; const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } ); export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} + extends React.HTMLAttributes, + VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ); + return
; } export { Badge, badgeVariants }; diff --git a/packages/popcorntime-ui/src/components/breadcrumb.tsx b/packages/popcorntime-ui/src/components/breadcrumb.tsx index dfddafcbe..61f27db17 100644 --- a/packages/popcorntime-ui/src/components/breadcrumb.tsx +++ b/packages/popcorntime-ui/src/components/breadcrumb.tsx @@ -1,109 +1,101 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" - -import { cn } from "@popcorntime/ui/lib/utils" +import { cn } from "@popcorntime/ui/lib/utils"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; +import type * as React from "react"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return