diff --git a/.eslintrc.js b/.eslintrc.js index 672e17b2a..ad402e2a4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,6 +10,7 @@ module.exports = { "plugin:react/recommended", "prettier", "plugin:cypress/recommended", + "plugin:storybook/recommended", ], parserOptions: { ecmaFeatures: { @@ -25,4 +26,22 @@ module.exports = { version: "detect", }, }, + overrides: [ + { + files: ["*.ts", "*.tsx"], + extends: [ + "plugin:react/recommended", + "prettier", + "plugin:cypress/recommended", + "plugin:storybook/recommended", + "plugin:@typescript-eslint/recommended", + ], + plugins: ["@typescript-eslint"], + rules: { + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-non-null-assertion": 0, + "react/display-name": "off", + }, + }, + ], }; diff --git a/.storybook/application.css b/.storybook/application.css new file mode 100644 index 000000000..846924d82 --- /dev/null +++ b/.storybook/application.css @@ -0,0 +1,26 @@ +@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap"); + +.pi-checkered-bg { + --size: 0.25rem; + --double: calc(var(--size) * 2); + --bg: #eee; + --fg: #ddd; + background: linear-gradient( + 45deg, + var(--fg) 25%, + transparent 25.1%, + transparent 74.9%, + var(--fg) 75% + ), + linear-gradient( + 45deg, + var(--fg) 25%, + transparent 25.1%, + transparent 74.9%, + var(--fg) 75% + ), + var(--bg); + background-repeat: repeat, repeat; + background-position: 0 0, var(--size) var(--size); + background-size: var(--double) var(--double), var(--double) var(--double); +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 000000000..06e73c92d --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,60 @@ +const path = require("path"); + +/** @type { import('@storybook/react-webpack5').StorybookConfig } */ +const config = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + staticDirs: ["../public"], + framework: { + name: "@storybook/react-webpack5", + options: { + builder: { + useSWC: true, + }, + }, + }, + core: { + disableTelemetry: true, + }, + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + "@storybook/addon-styling-webpack", + { + name: "@storybook/addon-styling-webpack", + + options: { + rules: [ + { + test: /\.css$/, + sideEffects: true, + use: [ + require.resolve("style-loader"), + { + loader: require.resolve("css-loader"), + options: { + importLoaders: 1, + }, + }, + { + loader: require.resolve("postcss-loader"), + options: { + postcssOptions: { + config: path.resolve( + __dirname, + "postcss.storybook.config.js" + ), + }, + }, + }, + ], + }, + ], + }, + }, + ], + docs: { + autodocs: "tag", + }, +}; +export default config; diff --git a/.storybook/manager.ts b/.storybook/manager.ts new file mode 100644 index 000000000..822a0514e --- /dev/null +++ b/.storybook/manager.ts @@ -0,0 +1,7 @@ +import { addons } from "@storybook/manager-api"; +import theme from "./theme"; +import "./application.css"; + +addons.setConfig({ + theme, +}); diff --git a/.storybook/postcss.storybook.config.js b/.storybook/postcss.storybook.config.js new file mode 100644 index 000000000..519e1c977 --- /dev/null +++ b/.storybook/postcss.storybook.config.js @@ -0,0 +1,9 @@ +module.exports = { + plugins: [ + "postcss-import", + "postcss-custom-properties", + "postcss-calc", + "autoprefixer", + "tailwindcss", + ], +}; diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 000000000..907f1ff35 --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { DocsContainer } from "@storybook/blocks"; +import { initialize, mswDecorator } from "msw-storybook-addon"; + +import "./styles.css"; +import theme from "./theme"; +import loadIcons from "../src/core/icons"; + +const docsContainer = ({ children, context, ...props }) => { + loadIcons(); + + return ( + + {children} + + ); +}; + +initialize({ + onUnhandledRequest: "bypass", +}); + +const preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + docs: { + theme, + container: docsContainer, + }, + }, + decorators: [ + mswDecorator, + (Story) => { + loadIcons(); + return Story(); + }, + ], +}; + +export default preview; diff --git a/.storybook/styles.css b/.storybook/styles.css new file mode 100644 index 000000000..d1d5916d4 --- /dev/null +++ b/.storybook/styles.css @@ -0,0 +1,10 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + +@import "../src/core/fonts/jetBrains-mono.css"; +@import "../src/core/fonts/manrope.css"; +@import "../src/core/styles.css"; +@import "../src/reset/styles.css"; + +@import "./application.css"; diff --git a/.storybook/theme.ts b/.storybook/theme.ts new file mode 100644 index 000000000..b745e56db --- /dev/null +++ b/.storybook/theme.ts @@ -0,0 +1,12 @@ +import { create } from "@storybook/theming/create"; + +const brandImage = + "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTA4IiBoZWlnaHQ9IjMyIiB2aWV3Qm94PSIwIDAgMTA4IDMyIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgoJPGcgY2xhc3M9InRleHQiPgoJCTxwYXRoIGQ9Ik02OC41MTk5IDIyLjY4NTZWMkg3Mi4yNTQ1VjkuNDg4OTVDNzMuNTE5NCA4LjI4NDYzIDc1LjE0ODggNy42MTY2OSA3Ni44NjkzIDcuNjE2NjlDODAuOTA3MyA3LjYxNjY5IDg0LjQ4OTggMTAuNjkzMiA4NC40ODk4IDE1LjMyODNDODQuNDg5OCAxOS45NjMzIDgwLjkwNzMgMjMuMDUgNzYuODY5MyAyMy4wNUM3NS4wNTc3IDIzLjA1IDczLjM0NzEgMjIuMzMxNCA3Mi4wNzIzIDIxLjAyNTlWMjIuNjg1Nkg2OC41MTk5Wk04MC43NTUzIDE1LjMyODNDODAuNzU1MyAxMi43Mzc1IDc4LjkxMzUgMTAuOTI2IDc2LjUwNDkgMTAuOTI2Qzc0LjE1NzEgMTAuOTI2IDcyLjM0NTYgMTIuNjQ2NCA3Mi4yNTQ1IDE1LjE0NjFWMTUuMzI4M0M3Mi4yNTQ1IDE3LjkxOSA3NC4wOTYyIDE5LjczMDUgNzYuNTA0OSAxOS43MzA1Qzc4LjkxMzUgMTkuNzMwNSA4MC43NTUzIDE3LjkxOSA4MC43NTUzIDE1LjMyODNaTTg2LjIyMDIgMjIuNjg1NlYySDg5Ljk1NDdWMjIuNjg1Nkg4Ni4yMjAyWk05NS4xNzY1IDI4LjA0OTNMOTcuMzUyNSAyMi44NTc3TDkxLjQ0MiA3Ljk4MTAzSDk1LjQ4TDk5LjI0NDcgMTguMzc0NEwxMDMuMDcgNy45ODEwM0gxMDcuMTY5TDk5LjE1MzYgMjguMDU5NEg5NS4xNzY1VjI4LjA0OTNaTTYyLjU4OTYgNy45ODEwM1Y5Ljg1MzI0QzYxLjI5NCA4LjQzNjQyIDU5LjQ4MjQgNy42MjY4NCA1Ny42MjA0IDcuNjI2ODRDNTMuNTgyNiA3LjYyNjg0IDUwIDEwLjcwMzQgNTAgMTUuMzM4NEM1MCAxOS45ODM1IDUzLjU4MjYgMjMuMDUgNTcuNjIwNCAyMy4wNUM1OS41NTM3IDIzLjA1IDYxLjM4NTEgMjIuMjEgNjIuNzEwOSAyMC43MDIxVjIyLjY5NTdINjUuOTY5N1Y3Ljk4MTAzSDYyLjU4OTZaTTYyLjIyNTMgMTUuMzI4M0M2Mi4yMjUzIDE3Ljg4ODcgNjAuMzgzNSAxOS43MzA1IDU3Ljk3NDggMTkuNzMwNUM1NS41NjYxIDE5LjczMDUgNTMuNzI0MiAxNy44ODg3IDUzLjcyNDIgMTUuMzI4M0M1My43MjQyIDEyLjc2NzkgNTUuNTY2MSAxMC45MjYgNTcuOTc0OCAxMC45MjZDNjAuMzIyNiAxMC45MjYgNjIuMTM0MiAxMi42NzY4IDYyLjIyNTMgMTUuMTQ2MVYxNS4zMjgzWiIgZmlsbD0iY3VycmVudENvbG9yIiAvPgoJPC9nPgoJPGcgY2xhc3M9InRyaWFuZ2xlIj4KCQk8cGF0aCBkPSJNMTkuMDk2OCAwTDMuMTE3MDEgMjkuMjQ3M0wwIDI3LjA2MTRMMTQuNzg1NiAwSDE5LjA5NjhaTTE5LjMxOTQgMEwzNS4yOTkyIDI5LjI0NzNMMzguNDE2MiAyNy4wNjE0TDIzLjYzMDYgMEgxOS4zMTk0WiIgZmlsbD0idXJsKCNncmFkaWVudCkiIC8+CgkJPHBhdGggZD0iTTM1LjA3NjUgMjkuNDE5NEwxOS4yMDgxIDE2Ljk5MThMMy4zMzk2NiAyOS40MTk0TDYuNTc4MTMgMzEuNjg2M0wxOS4yMDgxIDIxLjc5ODlMMzEuODM4MSAzMS42ODYzTDM1LjA3NjUgMjkuNDE5NFoiIGZpbGw9InVybCgjZ3JhZGllbnQpIiAvPgoJPC9nPgoJPGRlZnM+CgkJPGxpbmVhckdyYWRpZW50IGlkPSJncmFkaWVudCIgeDE9IjE5LjIwODEiIHkxPSIwIiB4Mj0iMyIgeTI9IjI4LjUiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KCQkJPHN0b3Agc3RvcC1jb2xvcj0iI0ZGMDkwMiIgLz4KCQkJPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRkY1MjE1IiAvPgoJCTwvbGluZWFyR3JhZGllbnQ+Cgk8L2RlZnM+Cjwvc3ZnPgo="; + +export default create({ + base: "light", + brandTitle: "Ably UI", + brandImage, + brandTarget: "_self", + fontBase: '"Manrope", "Open Sans", sans-serif', +}); diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..5a586b3d1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "css.lint.unknownAtRules": "ignore" +} diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 000000000..b24459644 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,5 @@ +declare module "*.png"; +declare module "*.svg" { + const content: string; + export default content; +} diff --git a/package.json b/package.json index 47318ccd2..66091f244 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,16 @@ "@babel/preset-env": "^7.12.1", "@babel/preset-react": "^7.12.5", "@prettier/plugin-ruby": "^1.5.2", + "@storybook/addon-essentials": "^7.6.4", + "@storybook/addon-interactions": "^7.6.4", + "@storybook/addon-links": "^7.6.4", + "@storybook/addon-styling-webpack": "^0.0.5", + "@storybook/blocks": "^7.6.4", + "@storybook/react": "^7.6.4", + "@storybook/react-webpack5": "^7.6.4", + "@storybook/test": "^7.6.4", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "autoprefixer": "^10.0.2", "babel-loader": "^8.2.0", "blink-diff": "^1.0.13", @@ -31,8 +41,12 @@ "eslint-config-prettier": "^6.15.0", "eslint-plugin-cypress": "^2.11.2", "eslint-plugin-react": "^7.21.5", + "eslint-plugin-storybook": "^0.6.15", + "extra-watch-webpack-plugin": "^1.0.3", "find-imports": "^1.1.0", "mini-css-extract-plugin": "^1.2.1", + "msw": "1.3.2", + "msw-storybook-addon": "^1.10.0", "null-loader": "^4.0.1", "postcss": "^8.1.10", "postcss-calc": "^7.0.5", @@ -40,8 +54,11 @@ "postcss-import": "^13.0.0", "postcss-loader": "^4.0.4", "prettier": "^2.3.0", + "storybook": "^7.6.4", + "style-loader": "^3.3.3", "svg-spritemap-webpack-plugin": "^3.7.1", "tailwindcss": "^3.3.6", + "typescript": "5.3.3", "webpack": "^5.3.2", "webpack-cli": "^4.2.0", "yargs": "^16.2.0" @@ -51,14 +68,16 @@ "build:verbose": "node scripts/build.js -v", "watch": "node scripts/build.js -w", "dev": "./scripts/cleanstart.sh", - "format:check": "yarn prettier -c *.js src src/**/*.jsx cypress", - "format:write": "yarn prettier -w *.js src src/**/*.jsx cypress", - "lint": "eslint *.js src src/**/*.jsx cypress", + "format:check": "yarn prettier -c *.{js,ts} src/**/*.{js,jsx,ts,tsx} cypress", + "format:write": "yarn prettier -w *.{js,ts} src/**/*.{js,jsx,ts,tsx} cypress", + "lint": "eslint *.{js,ts} src/**/*.{js,jsx,ts,tsx} cypress", "cy:open": "cypress open", "cy:headless": "cypress run --quiet", "update:all": "./scripts/update-dependents.sh", "pre-release": "./scripts/pre-release.sh", - "release": "./scripts/release.sh" + "release": "./scripts/release.sh", + "storybook": "storybook dev -p 6006 --no-version-updates", + "build-storybook": "storybook build" }, "dependencies": { "@mrtkrcm/cypress-plugin-snapshots": "https://github.com/mrtkrcm/cypress-plugin-snapshots#v1.13.0", @@ -87,5 +106,8 @@ "react", "view-components" ], - "author": "Ably Real-time Ltd " -} + "author": "Ably Real-time Ltd ", + "msw": { + "workerDirectory": "public" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js index dd8ad4d54..4e85a41ea 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,8 +1,7 @@ -module.exports = { - plugins: [ - "postcss-import", - "postcss-custom-properties", - "postcss-calc", - "autoprefixer", - ], +export default { + plugins: { + "postcss-import": {}, + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 000000000..51d85eeeb --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,303 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker (1.3.2). + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const INTEGRITY_CHECKSUM = '3d6b9f06410d179a7f7404d4bf4c3c70' +const activeClientIds = new Set() + +self.addEventListener('install', function () { + self.skipWaiting() +}) + +self.addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +self.addEventListener('message', async function (event) { + const clientId = event.source.id + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: INTEGRITY_CHECKSUM, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: true, + }) + break + } + + case 'MOCK_DEACTIVATE': { + activeClientIds.delete(clientId) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +self.addEventListener('fetch', function (event) { + const { request } = event + const accept = request.headers.get('accept') || '' + + // Bypass server-sent events. + if (accept.includes('text/event-stream')) { + return + } + + // Bypass navigation requests. + if (request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + // Generate unique request ID. + const requestId = Math.random().toString(16).slice(2) + + event.respondWith( + handleRequest(event, requestId).catch((error) => { + if (error.name === 'NetworkError') { + console.warn( + '[MSW] Successfully emulated a network error for the "%s %s" request.', + request.method, + request.url, + ) + return + } + + // At this point, any exception indicates an issue with the original request/response. + console.error( + `\ +[MSW] Caught an exception from the "%s %s" request (%s). This is probably not a problem with Mock Service Worker. There is likely an additional logging output above.`, + request.method, + request.url, + `${error.name}: ${error.message}`, + ) + }), + ) +}) + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event) + const response = await getResponse(event, client, requestId) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + ;(async function () { + const clonedResponse = response.clone() + sendToClient(client, { + type: 'RESPONSE', + payload: { + requestId, + type: clonedResponse.type, + ok: clonedResponse.ok, + status: clonedResponse.status, + statusText: clonedResponse.statusText, + body: + clonedResponse.body === null ? null : await clonedResponse.text(), + headers: Object.fromEntries(clonedResponse.headers.entries()), + redirected: clonedResponse.redirected, + }, + }) + })() + } + + return response +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +async function getResponse(event, client, requestId) { + const { request } = event + const clonedRequest = request.clone() + + function passthrough() { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const headers = Object.fromEntries(clonedRequest.headers.entries()) + + // Remove MSW-specific request headers so the bypassed requests + // comply with the server's CORS preflight check. + // Operate with the headers as an object because request "Headers" + // are immutable. + delete headers['x-msw-bypass'] + + return fetch(clonedRequest, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Bypass requests with the explicit bypass header. + // Such requests can be issued by "ctx.fetch()". + if (request.headers.get('x-msw-bypass') === 'true') { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const clientMessage = await sendToClient(client, { + type: 'REQUEST', + payload: { + id: requestId, + url: request.url, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + mode: request.mode, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.text(), + bodyUsed: request.bodyUsed, + keepalive: request.keepalive, + }, + }) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'MOCK_NOT_FOUND': { + return passthrough() + } + + case 'NETWORK_ERROR': { + const { name, message } = clientMessage.data + const networkError = new Error(message) + networkError.name = name + + // Rejecting a "respondWith" promise emulates a network error. + throw networkError + } + } + + return passthrough() +} + +function sendToClient(client, message) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [channel.port2]) + }) +} + +function sleep(timeMs) { + return new Promise((resolve) => { + setTimeout(resolve, timeMs) + }) +} + +async function respondWithMock(response) { + await sleep(response.delay) + return new Response(response.body, response) +} diff --git a/src/core/Code/Code.stories.tsx b/src/core/Code/Code.stories.tsx new file mode 100644 index 000000000..0a6454b64 --- /dev/null +++ b/src/core/Code/Code.stories.tsx @@ -0,0 +1,71 @@ +import Code from "./component.jsx"; + +export default { + title: "Components/Code", + component: Code, + tags: ["autodocs"], +}; + +export const Javascript = { + args: { + language: "javascript", + snippet: `var ably = new Ably.Realtime('1WChTA.mc0Biw:kNfiYG4KiPgmHHgH'); +var channel = ably.channels.get('web-pal'); + +// Subscribe to messages on channel +channel.subscribe('greeting', function(message) { + alert(message.data); +});`, + }, +}; + +export const Swift = { + args: { + language: "swift", + snippet: `let ably = ARTRealtime(key: "1WChTA.mc0Biw:kNfiYG4KiPgmHHgH") +let channel = ably.channels.get("web-pal") + +// Subscribe to messages on channel +channel.subscribe("greeting") { message in + print("\\(message.data)") +}`, + }, +}; + +export const Java = { + args: { + language: "java", + snippet: `AblyRealtime ably = new AblyRealtime("1WChTA.mc0Biw:kNfiYG4KiPgmHHgH"); +Channel channel = ably.channels.get("web-pal"); + +/* Subscribe to messages on channel */ + +MessageListener listener; +listener = new MessageListener() { + @Override + public void onMessage(Message message) { + System.out.print(message.data); + }; +}; +channel.subscribe("greeting", listener);`, + }, +}; + +export const Kotlin = { + args: { + language: "kotlin", + snippet: `var ably = new Ably.Realtime('1WChTA.mc0Biw:kNfiYG4KiPgmHHgH'); +val exampleConstraints = DefaultResolutionConstraints( + DefaultResolutionSet( // this constructor provides one Resolution for all states + Resolution( + accuracy = Accuracy.BALANCED, + desiredInterval = 1000L, + minimumDisplacement = 1.0 + ) + ), + proximityThreshold = DefaultProximity(spatial = 1.0), + batteryLevelThreshold = 10.0f, + lowBatteryMultiplier = 2.0f +)`, + }, +}; diff --git a/src/core/ContactFooter/ContactFooter.stories.tsx b/src/core/ContactFooter/ContactFooter.stories.tsx new file mode 100644 index 000000000..233f7f56f --- /dev/null +++ b/src/core/ContactFooter/ContactFooter.stories.tsx @@ -0,0 +1,11 @@ +import ContactFooter from "./component.jsx"; + +export default { + title: "Components/Contact Footer", + component: ContactFooter, + parameters: { + layout: "fullscreen", + }, +}; + +export const Default = {}; diff --git a/src/core/ContactFooter/component.css b/src/core/ContactFooter/component.css index e55d9cf9b..1976d1fb3 100644 --- a/src/core/ContactFooter/component.css +++ b/src/core/ContactFooter/component.css @@ -1,11 +1,9 @@ -@layer components { - .ui-contact-footer { - background-size: 100% 100%; - background-position: right center; - @apply w-full bg-gradient-active-orange; - } +.ui-contact-footer { + background-size: 100% 100%; + background-position: right center; + @apply w-full bg-gradient-active-orange; +} - .ui-contact-footer-box { - @apply p-24 sm:p-32 xl:p-40 bg-white flex flex-col justify-between rounded-sm; - } +.ui-contact-footer-box { + @apply p-24 sm:p-32 xl:p-40 bg-white flex flex-col justify-between rounded-sm; } diff --git a/src/core/CookieMessage/CookieMessage.stories.tsx b/src/core/CookieMessage/CookieMessage.stories.tsx new file mode 100644 index 000000000..72b141410 --- /dev/null +++ b/src/core/CookieMessage/CookieMessage.stories.tsx @@ -0,0 +1,12 @@ +import CookieMessage from "./component.jsx"; + +export default { + title: "Components/Cookie Message", + component: CookieMessage, + args: { + cookieId: "cookie-namespace", + noticeId: "cookie-message", + }, +}; + +export const Default = {}; diff --git a/src/core/CustomerLogos/CustomerLogos.stories.tsx b/src/core/CustomerLogos/CustomerLogos.stories.tsx new file mode 100644 index 000000000..190d72d40 --- /dev/null +++ b/src/core/CustomerLogos/CustomerLogos.stories.tsx @@ -0,0 +1,43 @@ +import CustomerLogos from "./component.jsx"; + +import hubspot from "../images/cust-logo-hubspot-mono-pos.svg"; +import webflow from "../images/cust-logo-webflow-col-pos.svg"; +import mentimeter from "../images/cust-logo-mentimeter-mono-pos.svg"; +import toyota from "../images/cust-logo-toyota-mono-pos.svg"; +import split from "../images/cust-logo-split-mono-pos.svg"; +import australian from "../images/cust-logo-ausopen-mono-pos.svg"; + +export default { + title: "Components/Customer Logos", + component: CustomerLogos, + args: { + companies: [ + { + label: "Hubspot", + logo: hubspot, + }, + { + label: "Webflow", + logo: webflow, + }, + { + label: "Mentimeter", + logo: mentimeter, + }, + { + label: "Toyota", + logo: toyota, + }, + { + label: "Split", + logo: split, + }, + { + label: "Australian Open", + logo: australian, + }, + ], + }, +}; + +export const Default = {}; diff --git a/src/core/DropdownMenu/DropdownMenu.stories.tsx b/src/core/DropdownMenu/DropdownMenu.stories.tsx new file mode 100644 index 000000000..48d768c02 --- /dev/null +++ b/src/core/DropdownMenu/DropdownMenu.stories.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import Icon from "../Icon/component.jsx"; +import DropdownMenu from "./component.jsx"; + +export default { + title: "Components/Dropdown Menu", + component: DropdownMenu, +}; + +export const Default = { + render: () => ( + + + Dropdown Menu Trigger + + + +

I am a child! 🐣

+
+ + +

+ Using plain HTML + +

+
+
+
+ ), +}; diff --git a/src/core/FeaturedLink/FeaturedLink.stories.tsx b/src/core/FeaturedLink/FeaturedLink.stories.tsx new file mode 100644 index 000000000..db75f0b94 --- /dev/null +++ b/src/core/FeaturedLink/FeaturedLink.stories.tsx @@ -0,0 +1,43 @@ +import FeaturedLink from "./component.jsx"; + +export default { + title: "Components/Featured Link", + component: FeaturedLink, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + args: { + url: "#", + children: "Featured link", + }, +}; + +export const Default = { + args: {}, +}; + +export const Reverse = { + args: { + reverse: true, + }, +}; + +export const Large = { + args: { + textSize: "text-p1", + }, +}; + +export const Small = { + args: { + textSize: "text-p3", + }, +}; + +export const Pink = { + args: { + iconColor: "text-pink-500", + additionalCSS: "text-pink-800", + }, +}; diff --git a/src/core/Flash/Flash.stories.tsx b/src/core/Flash/Flash.stories.tsx new file mode 100644 index 000000000..7e3311126 --- /dev/null +++ b/src/core/Flash/Flash.stories.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import Flash, { reducerFlashes } from "./component.jsx"; + +import { + attachStoreToWindow, + createRemoteDataStore, +} from "../remote-data-store.js"; +import { reducerBlogPosts } from "../remote-blogs-posts.js"; +import { reducerSessionData } from "../remote-session-data.js"; + +export default { + title: "Components/Flash", + component: Flash, + args: { + flashes: [ + ["success", "Congratulations! You've won the Oscar"], + ["notice", "This is a notice"], + ["error", "This is an error, very bad"], + ["alert", "This is an alert"], + ["info", "Some useful information, you are welcome"], + ], + }, +}; + +export const Default = { + render: (args) => { + const store = createRemoteDataStore({ + ...reducerBlogPosts, + ...reducerSessionData, + ...reducerFlashes, + }); + + attachStoreToWindow(store); + + return ; + }, +}; diff --git a/src/core/Flash/component.css b/src/core/Flash/component.css index c200fbe8a..4ed245735 100644 --- a/src/core/Flash/component.css +++ b/src/core/Flash/component.css @@ -1,25 +1,23 @@ -@layer components { - .ui-flash { - @apply w-full fixed; - top: 5.5rem; - z-index: calc(var(--stacking-context-page-meganav) - 10); - transition: margin-top 200ms; - } +.ui-flash { + @apply w-full fixed; + top: 5.5rem; + z-index: calc(var(--stacking-context-page-meganav) - 10); + transition: margin-top 200ms; +} - .ui-flash-message { - @apply font-sans font-light antialiased max-w-screen-xl mx-auto mt-8 opacity-0 relative; - transition: opacity 200ms, transform 200ms, height 200ms 200ms, - margin-top 200ms 200ms; - transform: translateY(-200%) rotateX(-90deg); - } +.ui-flash-message { + @apply font-sans font-light antialiased max-w-screen-xl mx-auto mt-8 opacity-0 relative; + transition: opacity 200ms, transform 200ms, height 200ms 200ms, + margin-top 200ms 200ms; + transform: translateY(-200%) rotateX(-90deg); +} - /* dynamic content inside flash, can't add classes */ - .ui-flash-text a { - @apply underline; - } +/* dynamic content inside flash, can't add classes */ +.ui-flash-text a { + @apply underline; +} - .ui-flash-message-enter { - @apply opacity-100; - transform: translateY(0) rotateX(0); - } +.ui-flash-message-enter { + @apply opacity-100; + transform: translateY(0) rotateX(0); } diff --git a/src/core/Flash/component.jsx b/src/core/Flash/component.jsx index 3266eb415..91221702a 100644 --- a/src/core/Flash/component.jsx +++ b/src/core/Flash/component.jsx @@ -3,9 +3,10 @@ import DOMPurify from "dompurify"; import T from "prop-types"; import { nanoid } from "nanoid/non-secure"; -import { getRemoteDataStore } from "../remote-data-store"; +import { getRemoteDataStore } from "../remote-data-store.js"; import ConnectStateWrapper from "../ConnectStateWrapper/component.jsx"; import Icon from "../Icon/component.jsx"; +import "./component.css"; const REDUCER_KEY = "flashes"; const FLASH_DATA_ID = "ui-flashes"; diff --git a/src/core/Footer/Footer.stories.tsx b/src/core/Footer/Footer.stories.tsx new file mode 100644 index 000000000..8de0e823f --- /dev/null +++ b/src/core/Footer/Footer.stories.tsx @@ -0,0 +1,26 @@ +import Footer from "./component.jsx"; + +import ablyStack from "../images/ably-stack.svg"; +import highestPerformer from "../images/high-performer-2023.svg"; +import bestSupport from "../images/best-support-2023.svg"; +import fastestImplementation from "../images/fastest-implementation-2023.svg"; +import highestUserAdoption from "../images/highest-user-adoption-2023.svg"; + +export default { + title: "Components/Footer", + component: Footer, + parameters: { + layout: "fullscreen", + }, + args: { + paths: { + ablyStack, + highestPerformer, + bestSupport, + fastestImplementation, + highestUserAdoption, + }, + }, +}; + +export const Default = {}; diff --git a/src/core/Footer/component.css b/src/core/Footer/component.css index a4c98f3a2..f4473ce93 100644 --- a/src/core/Footer/component.css +++ b/src/core/Footer/component.css @@ -1,33 +1,31 @@ -@layer components { - .ui-footer-col-title { - @apply font-mono text-overline2 p-menu-row-title font-medium uppercase tracking-widen-0.16; - } +.ui-footer-col-title { + @apply font-mono text-overline2 p-menu-row-title font-medium uppercase tracking-widen-0.16; +} - .ui-footer-menu-row-link { - @apply text-menu3 text-cool-black font-sans font-medium hover:text-gui-hover block; - } +.ui-footer-menu-row-link { + @apply text-menu3 text-cool-black font-sans font-medium hover:text-gui-hover block; +} - .ui-footer-link { - @apply text-gui-default hover:text-gui-hover text-menu3 font-sans font-medium; - } +.ui-footer-link { + @apply text-gui-default hover:text-gui-hover text-menu3 font-sans font-medium; +} - .ui-footer-compliance-text { - font-size: 12px; - } +.ui-footer-compliance-text { + font-size: 12px; +} - .ui-footer-tick-icon { - min-width: 1.5rem; - } +.ui-footer-tick-icon { + min-width: 1.5rem; +} - @media (max-width: 1040px) { - .ui-footer-bottom-links { - @apply pb-40; - } +@media (max-width: 1040px) { + .ui-footer-bottom-links { + @apply pb-40; } +} - @media screen { - .ui-footer-glassdoor { - display: none; - } +@media screen { + .ui-footer-glassdoor { + display: none; } } diff --git a/src/core/Footer/component.jsx b/src/core/Footer/component.jsx index 7cf799360..1f3a061e5 100644 --- a/src/core/Footer/component.jsx +++ b/src/core/Footer/component.jsx @@ -3,6 +3,7 @@ import T from "prop-types"; import Icon from "../Icon/component.jsx"; import _absUrl from "../url-base"; +import "./component.css"; export default function Footer({ paths, urlBase }) { const absUrl = (path) => _absUrl(path, urlBase); @@ -141,7 +142,7 @@ export default function Footer({ paths, urlBase }) {