diff --git a/examples/index.html b/examples/index.html index e064db158..be1baaa86 100644 --- a/examples/index.html +++ b/examples/index.html @@ -12,6 +12,7 @@

Example Apps with Seam React Components

diff --git a/examples/vite-env.d.ts b/examples/vite-env.d.ts index 691f7a317..7b56feb0d 100644 --- a/examples/vite-env.d.ts +++ b/examples/vite-env.d.ts @@ -5,3 +5,5 @@ interface ImportMeta { SEAM_USER_IDENTIFIER_KEY: string } } + +declare module '@seamapi/react/elements.js' {} diff --git a/examples/vite.config.ts b/examples/vite.config.ts index 0a371200d..72bbf5a15 100644 --- a/examples/vite.config.ts +++ b/examples/vite.config.ts @@ -26,6 +26,9 @@ export default defineConfig(async ({ command, mode }) => { ], resolve: { alias: { + '@seamapi/react/elements.js': fileURLToPath( + new URL('../src/elements.js', import.meta.url) + ), '@seamapi/react/index.css': fileURLToPath( new URL('../src/index.scss', import.meta.url) ), diff --git a/examples/web-components/index.html b/examples/web-components/index.html new file mode 100644 index 000000000..a4d7c4cde --- /dev/null +++ b/examples/web-components/index.html @@ -0,0 +1,23 @@ + + + + + + Web components example - Seam React Components + + +
+ + +
+ + + diff --git a/examples/web-components/src/env.d.ts b/examples/web-components/src/env.d.ts new file mode 100644 index 000000000..6d8d45880 --- /dev/null +++ b/examples/web-components/src/env.d.ts @@ -0,0 +1,11 @@ +import 'vite/client' + +interface ImportMetaEnv { + readonly SEAM_ENDPOINT: string + readonly SEAM_PUBLISHABLE_KEY: string + readonly SEAM_USER_IDENTIFIER_KEY: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/examples/web-components/src/main.ts b/examples/web-components/src/main.ts new file mode 100644 index 000000000..90166b6fc --- /dev/null +++ b/examples/web-components/src/main.ts @@ -0,0 +1,2 @@ +import '@seamapi/react/index.css' +import '@seamapi/react/elements.js' diff --git a/package-lock.json b/package-lock.json index 0e2c67284..91b7170d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.2", + "@r2wc/react-to-web-component": "^2.0.2", "@seamapi/fake-seam-connect": "^0.7.2", "@storybook/addon-essentials": "^7.0.2", "@storybook/addon-interactions": "^7.0.2", @@ -4323,6 +4324,25 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@r2wc/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@r2wc/core/-/core-1.0.0.tgz", + "integrity": "sha512-P/P1YXpCXESnJKxFGXDZFrldov3T5wqeJSgQNWMgTkuV4CA9C+gVwv2guadkB7h5PuFyJxaLLbK6vl2VAUltpA==", + "dev": true + }, + "node_modules/@r2wc/react-to-web-component": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@r2wc/react-to-web-component/-/react-to-web-component-2.0.2.tgz", + "integrity": "sha512-HxgWXh6aipgvZ7l31m+AN0mM8KI5jkiCEBnSMBl/UKibVrhFgtsumL8fmLd5/q+OHBJKK0w4e5sAmzY1yu8dpA==", + "dev": true, + "dependencies": { + "@r2wc/core": "^1.0.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@seamapi/fake-seam-connect": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/@seamapi/fake-seam-connect/-/fake-seam-connect-0.7.2.tgz", diff --git a/package.json b/package.json index a6c2bc54a..9c063840f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "./hooks.js": { "import": "./hooks.js" }, + "./elements.js": { + "import": "./dist/elements.js" + }, "./index.css": { "import": "./index.css" } @@ -42,6 +45,7 @@ "index.min.css.map", "lib", "src", + "dist", "!test", "!**/*.test.ts", "!**/*.test.tsx", @@ -52,11 +56,13 @@ "scripts": { "start": "concurrently --raw --kill-others npm:examples:storybook npm:storybook", "dev": "npm run start", - "build": "npm run build:js", + "build": "npm run build:entrypoints", "prebuild": "del 'index.*' 'hooks.*' lib", - "postbuild": "concurrently 'node ./index.js' 'node ./hooks.js'", + "postbuild": "concurrently --raw --group 'node ./index.js' 'node ./hooks.js'", + "build:entrypoints": "npm run build:js", + "prebuild:entrypoints": "npm run build:css", + "postbuild:entrypoints": "vite build", "build:js": "tsc --project tsconfig.build.json", - "prebuild:js": "npm run build:css", "postbuild:js": "tsc-alias --project tsconfig.build.json", "build:css": "sass --load-path=node_modules src/index.scss:index.css", "postbuild:css": "sass --style=compressed --load-path=node_modules src/index.scss:index.min.css", @@ -71,7 +77,7 @@ "lint": "eslint --ignore-path .gitignore --ext .js,.cjs,.mjs,.ts,.tsx .", "prelint": "prettier --check --ignore-path .gitignore .", "postlint": "stylelint '**/*.scss'", - "prepack": "echo \"export default '$(jq -r .version < package.json)'\" > lib/version.js", + "prepack": "echo \"const seamapiReactVersion = '$(jq -r .version < package.json)'\n\nexport default seamapiReactVersion\" > src/lib/version.ts && echo \"declare const seamapiReactVersion = \\\"$(jq -r .version < package.json)\\\";\nexport default seamapiReactVersion;\" > lib/version.d.ts && echo -n \"const seamapiReactVersion = '$(jq -r .version < package.json)';\nexport default seamapiReactVersion;\n//# sourceMappingURL=version.js.map\" > lib/version.js", "postversion": "git push --follow-tags", "storybook": "storybook dev --port 6006", "storybook:docs": "storybook dev --docs --port 6007", @@ -112,6 +118,7 @@ "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.2", "@seamapi/fake-seam-connect": "^0.7.2", + "@r2wc/react-to-web-component": "^2.0.2", "@storybook/addon-essentials": "^7.0.2", "@storybook/addon-interactions": "^7.0.2", "@storybook/addon-links": "^7.0.2", diff --git a/src/elements.ts b/src/elements.ts new file mode 100644 index 000000000..cd05af4c7 --- /dev/null +++ b/src/elements.ts @@ -0,0 +1,27 @@ +import { defineCustomElement, type ElementDefinition } from 'lib/element.js' +import * as components from 'lib/seam/components/elements.js' + +const elementDefinitions = components as unknown as Record< + string, + Partial +> + +for (const key of Object.keys(elementDefinitions)) { + const elementDefinition = elementDefinitions[key] + + if (elementDefinition == null) { + throw new Error(`Missing element element definition for ${key}`) + } + + const { name, Component, props } = elementDefinition + + if (name == null) { + throw new Error(`Missing element name for ${key}`) + } + + if (Component == null) { + throw new Error(`Missing element Component for ${key}`) + } + + defineCustomElement({ name, Component, props }) +} diff --git a/src/lib/element.tsx b/src/lib/element.tsx new file mode 100644 index 000000000..842db5c0c --- /dev/null +++ b/src/lib/element.tsx @@ -0,0 +1,86 @@ +import r2wc from '@r2wc/react-to-web-component' +import type { ComponentType } from 'react' +import type { Container } from 'react-dom' + +import { + SeamProvider, + type SeamProviderPropsWithClientSessionToken, + type SeamProviderPropsWithPublishableKey, +} from 'lib/seam/SeamProvider.js' + +declare global { + // eslint-disable-next-line no-var + var disableSeamCssInjection: boolean | undefined + // eslint-disable-next-line no-var + var disableSeamFontInjection: boolean | undefined + // eslint-disable-next-line no-var + var unminifiySeamCss: boolean | undefined +} + +export interface ElementDefinition { + name: string + Component: Parameters[0] + props?: ElementProps> +} + +export type ElementProps = Partial< + Record +> + +type ProviderProps = SeamProviderPropsWithPublishableKey & + SeamProviderPropsWithClientSessionToken + +const providerProps: ElementProps = { + publishableKey: 'string', + userIdentifierKey: 'string', + clientSessionToken: 'string', + disableCssInjection: 'boolean', + disableFontInjection: 'boolean', + unminifiyCss: 'boolean', +} + +export const defineCustomElement = ({ + name, + Component, + props = {}, +}: ElementDefinition): void => { + const element = r2wc(withProvider(Component), { + props: { + ...props, + ...providerProps, + }, + }) + globalThis?.customElements?.define(name, element) +} + +function withProvider

( + Component: ComponentType

+) { + return ({ + publishableKey, + userIdentifierKey, + clientSessionToken, + disableCssInjection, + disableFontInjection, + unminifiyCss, + container: _container, + ...props + }: ProviderProps & { container: Container } & P): JSX.Element | null => { + return ( + + + + ) + } +} diff --git a/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.element.ts b/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.element.ts new file mode 100644 index 000000000..77e8a1d7d --- /dev/null +++ b/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.element.ts @@ -0,0 +1,12 @@ +import type { ElementProps } from 'lib/element.js' + +import type { AccessCodeDetailsProps } from './AccessCodeDetails.js' + +export const name = 'seam-access-code-details' + +export const props: ElementProps = { + accessCodeId: 'string', + onBack: 'function', +} + +export { AccessCodeDetails as Component } from './AccessCodeDetails.js' diff --git a/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts b/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts new file mode 100644 index 000000000..190c371be --- /dev/null +++ b/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts @@ -0,0 +1,12 @@ +import type { ElementProps } from 'lib/element.js' + +import type { AccessCodeTableProps } from './AccessCodeTable.js' + +export const name = 'seam-access-code-table' + +export const props: ElementProps = { + deviceId: 'string', + onBack: 'function', +} + +export { AccessCodeTable as Component } from './AccessCodeTable.js' diff --git a/src/lib/seam/components/DeviceDetails/DeviceDetails.element.ts b/src/lib/seam/components/DeviceDetails/DeviceDetails.element.ts new file mode 100644 index 000000000..16a75c974 --- /dev/null +++ b/src/lib/seam/components/DeviceDetails/DeviceDetails.element.ts @@ -0,0 +1,12 @@ +import type { ElementProps } from 'lib/element.js' + +import type { DeviceDetailsProps } from './DeviceDetails.js' + +export const name = 'seam-device-details' + +export const props: ElementProps = { + deviceId: 'string', + onBack: 'function', +} + +export { DeviceDetails as Component } from './DeviceDetails.js' diff --git a/src/lib/seam/components/DeviceTable/DeviceTable.element.ts b/src/lib/seam/components/DeviceTable/DeviceTable.element.ts new file mode 100644 index 000000000..88e01a731 --- /dev/null +++ b/src/lib/seam/components/DeviceTable/DeviceTable.element.ts @@ -0,0 +1,11 @@ +import type { ElementProps } from 'lib/element.js' + +import type { DeviceTableProps } from './DeviceTable.js' + +export const name = 'seam-device-table' + +export const props: ElementProps = { + onBack: 'function', +} + +export { DeviceTable as Component } from './DeviceTable.js' diff --git a/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.tsx b/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.tsx index 52ac8a157..456ffbbf5 100644 --- a/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.tsx +++ b/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceContent.tsx @@ -81,7 +81,10 @@ export function SupportedDeviceContent({ function EmptyResult({ filterValue, resetFilterValue, -}: Pick) { +}: Pick< + SupportedDeviceContentProps, + 'filterValue' | 'resetFilterValue' +>): JSX.Element { const noMatchingRows = ( <>

{t.noMatch}

diff --git a/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.element.ts b/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.element.ts new file mode 100644 index 000000000..9347a952e --- /dev/null +++ b/src/lib/seam/components/SupportedDeviceTable/SupportedDeviceTable.element.ts @@ -0,0 +1,11 @@ +import type { ElementProps } from 'lib/element.js' + +import type { SupportedDeviceTableProps } from './SupportedDeviceTable.js' + +export const name = 'seam-supported-device-table' + +export const props: ElementProps = { + cannotFilter: 'boolean', +} + +export { SupportedDeviceTable as Component } from './SupportedDeviceTable.js' diff --git a/src/lib/seam/components/elements.ts b/src/lib/seam/components/elements.ts new file mode 100644 index 000000000..5f7542a2c --- /dev/null +++ b/src/lib/seam/components/elements.ts @@ -0,0 +1,5 @@ +export * as AccessCodeDetails from './AccessCodeDetails/AccessCodeDetails.element.js' +export * as AccessCodeTable from './AccessCodeTable/AccessCodeTable.element.js' +export * as DeviceDetails from './DeviceDetails/DeviceDetails.element.js' +export * as DeviceTable from './DeviceTable/DeviceTable.element.js' +export * as SupportedDeviceTable from './SupportedDeviceTable/SupportedDeviceTable.element.js' diff --git a/src/lib/version.ts b/src/lib/version.ts index 7b8595488..61782bde3 100644 --- a/src/lib/version.ts +++ b/src/lib/version.ts @@ -1 +1,3 @@ -export default null +const seamapiReactVersion = null + +export default seamapiReactVersion diff --git a/src/stories/Introduction.mdx b/src/stories/Introduction.mdx index bfc50240c..6c29f1f14 100644 --- a/src/stories/Introduction.mdx +++ b/src/stories/Introduction.mdx @@ -20,3 +20,4 @@ import seamWordmarkS from './assets/seam-wordmark-s.webp' Seam React component library interactive documentation. - Basic Example App ([source](https://github.com/seamapi/react/tree/main/examples/basic/)) +- Web Components Example App ([source](https://github.com/seamapi/react/tree/main/examples/web-components/)) diff --git a/tsconfig.build.json b/tsconfig.build.json index e90237b25..c99151e3f 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -11,6 +11,10 @@ "files": ["src/index.ts", "src/hooks.ts"], "include": ["src/**/*"], "exclude": [ + "src/elements.ts", + "src/lib/element.tsx", + "src/lib/seam/components/elements.ts", + "**/*.element.ts", "**/*.test.ts", "**/*.test.tsx", "**/*.stories.ts", diff --git a/tsconfig.json b/tsconfig.json index 8608913b1..18c823da5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -35,7 +35,8 @@ "src/**/*", "test/**/*", "examples/**/*", + "api/**/*", ".storybook/**/*", - "api/**/*" + "vite.config.ts" ] } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..30d015ce1 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,38 @@ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' + +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' + +export default defineConfig(async () => { + const pkg = await readPackageJson() + const version: string = pkg.version + return { + plugins: [ + tsconfigPaths(), + // @ts-expect-error https://github.com/vitejs/vite-plugin-react/issues/104 + react(), + ], + define: { + 'process.env.NODE_ENV': "'production'", + 'const seamapiReactVersion = null': `const seamapiReactVersion = '${version}'`, + }, + build: { + outDir: fileURLToPath(new URL('./dist', import.meta.url)), + sourcemap: true, + lib: { + entry: fileURLToPath(new URL('./src/elements.ts', import.meta.url)), + fileName: 'elements', + formats: ['es'], + }, + }, + } +}) + +const readPackageJson = async () => { + const pkgBuff = await readFile( + fileURLToPath(new URL('package.json', import.meta.url)) + ) + return JSON.parse(pkgBuff.toString()) +}