├── CODEOWNERS ├── .eslintignore ├── tsconfig.test.json ├── src ├── widget.ts ├── store │ ├── utils │ │ ├── delay.ts │ │ ├── isNotNullable.ts │ │ ├── index.ts │ │ └── createShallowSelector.ts │ ├── slices │ │ └── hello.ts │ ├── index.ts │ └── api │ │ └── base.ts ├── contract.ts ├── root.component.tsx ├── components │ ├── widgets │ │ ├── index.tsx │ │ └── get.tsx │ ├── localAndSessionStorage │ │ ├── index.tsx │ │ └── localAndSessionStorage1FE.tsx │ ├── misc │ │ ├── widgetContainer.tsx │ │ ├── visuallyHidden.tsx │ │ ├── skeletonLoader.tsx │ │ ├── resultElementBoundary.tsx │ │ ├── helpers.ts │ │ └── utils.tsx │ ├── common │ │ ├── styles │ │ │ ├── UtilityTooltip.styles.ts │ │ │ └── UtilitySection.styles.ts │ │ ├── UtilitySection.tsx │ │ └── UtilityTooltip.tsx │ ├── experience │ │ ├── index.tsx │ │ ├── setTitle.tsx │ │ └── getTitle.tsx │ ├── routes │ │ ├── styles │ │ │ ├── utilsDemo.styles.ts │ │ │ └── home.styles.ts │ │ ├── utilsDemo.tsx │ │ └── home.tsx │ ├── router.tsx │ ├── platformPropsImport │ │ └── comparePlatformProps.tsx │ ├── context │ │ └── context.tsx │ ├── customLogger │ │ └── index.tsx │ ├── eventBus │ │ └── eventBus.tsx │ └── appLoadTime │ │ └── appLoadTime.tsx ├── types │ └── global.d.ts ├── sharedStyles.ts ├── root.component.test.tsx ├── app1.tsx ├── withProvider.tsx ├── declarations.d.ts └── assets │ └── 1fe-logo.svg ├── .yarnrc.yml ├── test-results └── .last-run.json ├── .prettierignore ├── playwright.config.ts ├── .prettierrc ├── .1fe.config.ts ├── jest.config.js ├── tsconfig.json ├── tests └── exampleVisual.spec.ts ├── .github └── workflows │ ├── widget-ci.yml │ └── rollback-widget.yml ├── LICENSE ├── .eslintrc.cjs ├── .gitignore ├── package.json └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @docusign/1fe 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends" 3 | } -------------------------------------------------------------------------------- /src/widget.ts: -------------------------------------------------------------------------------- 1 | import Widget from "./app1"; 2 | 3 | export default Widget; 4 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 2 | nodeLinker: node-modules 3 | -------------------------------------------------------------------------------- /test-results/.last-run.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "passed", 3 | "failedTests": [] 4 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .prettierignore 3 | yarn.lock 4 | yarn-error.log 5 | package-lock.json 6 | dist 7 | coverage 8 | pnpm-lock.yaml -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@playwright/test"; 2 | 3 | export default defineConfig({ 4 | testDir: "./tests", 5 | }); 6 | -------------------------------------------------------------------------------- /src/store/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export async function delay(ms: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /src/store/utils/isNotNullable.ts: -------------------------------------------------------------------------------- 1 | export const isNotNullable = (value: T | null | undefined): value is T => { 2 | return value !== null && value !== undefined; 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": false, 6 | "jsxSingleQuote": false, 7 | "endOfLine": "lf", 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /src/store/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createShallowSelector"; 2 | export * from "./delay"; 3 | // export * from './environment'; 4 | // export * from './invariant'; 5 | export * from "./isNotNullable"; 6 | -------------------------------------------------------------------------------- /src/contract.ts: -------------------------------------------------------------------------------- 1 | import { PlatformPropsType } from "@1fe/shell"; 2 | 3 | export type HostPropsContract = Record; 4 | 5 | export type WidgetProps = { 6 | host: HostPropsContract; 7 | platform: PlatformPropsType; 8 | }; 9 | -------------------------------------------------------------------------------- /src/root.component.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface RootProps { 4 | name: string; 5 | } 6 | const Root: React.FC = ({ name }) => { 7 | return
{name} is mounted!
; 8 | }; 9 | 10 | export default Root; 11 | -------------------------------------------------------------------------------- /src/components/widgets/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Get } from "./get"; 3 | 4 | export const Widgets: React.FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /.1fe.config.ts: -------------------------------------------------------------------------------- 1 | import { OneFeConfiguration } from "@1fe/cli"; 2 | import { getBaseConfig } from "@1fe/sample-widget-base-config"; // this is the redistributed package for the organization 3 | 4 | const configuration: OneFeConfiguration = { 5 | baseConfig: getBaseConfig, 6 | }; 7 | 8 | export default configuration; 9 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is a workaround for a emotion-jsx-runtime issue. 3 | * @see https://github.com/emotion-js/emotion/issues/3049#issuecomment-1576037494 4 | */ 5 | 6 | declare module "@emotion/react/jsx-runtime" { 7 | namespace JSX { 8 | type ElementType = React.JSX.ElementType; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: "ts-jest", 3 | testEnvironment: "jsdom", 4 | moduleFileExtensions: ["ts", "tsx", "js", "jsx"], 5 | transform: { 6 | "^.+\\.(ts|tsx)$": "ts-jest", 7 | }, 8 | setupFilesAfterEnv: ["@testing-library/jest-dom"], 9 | testPathIgnorePatterns: ["/node_modules/", "/tests/"], 10 | }; 11 | -------------------------------------------------------------------------------- /src/sharedStyles.ts: -------------------------------------------------------------------------------- 1 | export const utilityCard = { 2 | width: "100%", 3 | maxWidth: "800px", 4 | }; 5 | 6 | export const flexProps = { 7 | gap: 12, 8 | wrap: true, 9 | }; 10 | 11 | export const colors = { 12 | primary: "#1890ff", // Used in 8+ components 13 | textSecondary: "#666", // Used in 5+ components 14 | } as const; 15 | -------------------------------------------------------------------------------- /src/components/localAndSessionStorage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LocalAndSessionStorage1FE } from "./localAndSessionStorage1FE"; 3 | 4 | export const LocalAndSessionStorage: React.FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/misc/widgetContainer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | // this matches the styling in @internal/generic-child-widget 4 | export const WidgetContainer = styled.div({ 5 | width: "600px", 6 | height: "auto", 7 | padding: "10px", 8 | border: "1px solid black", 9 | overflow: "visible", 10 | marginTop: "20px", 11 | }); 12 | -------------------------------------------------------------------------------- /src/root.component.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | import "@testing-library/jest-dom"; 3 | import Root from "./root.component"; 4 | 5 | describe("Root component", () => { 6 | it("should be in the document", () => { 7 | const { getByText } = render(); 8 | expect(getByText(/Testapp is mounted!/i)).toBeInTheDocument(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/components/common/styles/UtilityTooltip.styles.ts: -------------------------------------------------------------------------------- 1 | export const tooltipContent = { 2 | maxWidth: 300, 3 | }; 4 | 5 | export const tooltipTitle = { 6 | fontWeight: "bold", 7 | marginBottom: 4, 8 | }; 9 | 10 | export const tooltipDescription = { 11 | marginBottom: 8, 12 | }; 13 | 14 | export const tooltipApi = { 15 | fontSize: "11px", 16 | opacity: 0.8, 17 | fontFamily: "monospace", 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/misc/visuallyHidden.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | /* 4 | Styles to visually hide text while still allowing 5 | it to be available to assistive technology. 6 | */ 7 | export const VisuallyHidden = styled.p({ 8 | clip: "rect(0 0 0 0)", 9 | clipPath: "inset(50%)", 10 | height: "1px", 11 | overflow: "hidden", 12 | position: "absolute", 13 | whiteSpace: "nowrap", 14 | width: "1px", 15 | }); 16 | -------------------------------------------------------------------------------- /src/store/utils/createShallowSelector.ts: -------------------------------------------------------------------------------- 1 | import { shallowEqual } from "react-redux"; 2 | import { createSelectorCreator, defaultMemoize } from "reselect"; 3 | 4 | /** 5 | * Creates a selector that uses shallow equality instead of deep equality. 6 | * Useful when combining multiple selectors into a composite state. 7 | */ 8 | export const createShallowSelector = createSelectorCreator( 9 | defaultMemoize, 10 | shallowEqual, 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/misc/skeletonLoader.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { Skeleton } from "antd"; 3 | 4 | const FlexDiv = styled.div` 5 | display: flex; 6 | `; 7 | 8 | export const SkeletonLoader = () => ( 9 | 10 |
11 | 12 | 18 | 19 |
20 |
21 | ); 22 | -------------------------------------------------------------------------------- /src/components/common/styles/UtilitySection.styles.ts: -------------------------------------------------------------------------------- 1 | export const section = { 2 | marginBottom: 24, 3 | }; 4 | 5 | export const header = { 6 | display: "flex", 7 | alignItems: "center", 8 | gap: 8, 9 | marginBottom: 12, 10 | }; 11 | 12 | export const title = { 13 | margin: 0, 14 | fontSize: 16, 15 | fontWeight: 600, 16 | }; 17 | 18 | export const helpIcon = { 19 | cursor: "help", 20 | }; 21 | 22 | export const description = { 23 | margin: "0 0 16px 0", 24 | fontSize: 14, 25 | lineHeight: 1.4, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/misc/resultElementBoundary.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | 3 | /* 4 | Styles for
 text content to make it more responsive. 
 5 | Prevents horizontal scroll bars on the entire window, 
 6 | forcing content to scroll within its own container if it 
 7 | cannot wrap.
 8 | */
 9 | 
10 | export const ResultElementBoundary = styled.pre({
11 |   /* offset max-width to account for vertical scroll bar */
12 |   maxWidth: "calc(100vw - 40px)",
13 |   whiteSpace: "pre-wrap",
14 |   overflowWrap: "break-word",
15 | });
16 | 


--------------------------------------------------------------------------------
/src/app1.tsx:
--------------------------------------------------------------------------------
 1 | import { platformProps } from "@1fe/shell";
 2 | import React, { useEffect } from "react";
 3 | import { Router as Widget } from "./components/router";
 4 | import { withProvider } from "./withProvider";
 5 | import { WidgetProps } from "./contract";
 6 | 
 7 | const RootWrapper: React.FC = (props) => {
 8 |   useEffect(() => {
 9 |     platformProps.utils.appLoadTime.end();
10 |     console.log("props", props);
11 |   }, []);
12 | 
13 |   return ;
14 | };
15 | 
16 | export default withProvider(RootWrapper);
17 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "extends": "ts-config-single-spa",
 3 |   "compilerOptions": {
 4 |     "jsx": "react-jsx",
 5 |     "jsxImportSource": "@emotion/react",
 6 |     "declaration": true,
 7 |     "declarationDir": "dist",
 8 |     "skipLibCheck": true,
 9 |     "target": "esnext",
10 |     "emitDeclarationOnly": false,
11 |     "paths": {
12 |       "react": ["../../node_modules/@types/react"],
13 |       "react-dom": ["../../node_modules/@types/react-dom"]
14 |     }
15 |   },
16 |   "include": ["src/**/*"],
17 |   "exclude": ["src/**/*.test*"]
18 | }
19 | 


--------------------------------------------------------------------------------
/src/store/slices/hello.ts:
--------------------------------------------------------------------------------
 1 | import { createSlice } from "@reduxjs/toolkit";
 2 | 
 3 | const initialState = {
 4 |   greeting: "Welcome to 1FE Starter Kit",
 5 | };
 6 | 
 7 | export const helloSlice = createSlice({
 8 |   name: "hello",
 9 |   initialState,
10 |   reducers: {
11 |     changeGreeting: (state, action) => {
12 |       state.greeting = action.payload;
13 |     },
14 |   },
15 | });
16 | 
17 | // Action creators are generated for each case reducer function
18 | export const { changeGreeting } = helloSlice.actions;
19 | 
20 | export default helloSlice.reducer;
21 | 


--------------------------------------------------------------------------------
/src/components/misc/helpers.ts:
--------------------------------------------------------------------------------
 1 | // This util is only to be used by the 1FE team for our e2e tests.
 2 | // If you are looking for this functionality to read cookies on the client, please reach out to #1fe-help.
 3 | export const getCookie = (name: string): string | null => {
 4 |   const cookies = document.cookie.split(";");
 5 |   for (let i = 0; i < cookies.length; i++) {
 6 |     const cookie = cookies[i];
 7 |     const [key, value] = cookie.split("=").map((c) => c.trim());
 8 |     if (key === name) {
 9 |       return value;
10 |     }
11 |   }
12 |   return null;
13 | };
14 | 


--------------------------------------------------------------------------------
/tests/exampleVisual.spec.ts:
--------------------------------------------------------------------------------
 1 | import { test, expect } from "@playwright/test";
 2 | import packageJson from "../package.json" with { type: "json" };
 3 | 
 4 | const version = process.env.WIDGET_VERSION || "1.0.1";
 5 | 
 6 | test("should display the expected string on the bathtub page", async ({
 7 |   page,
 8 | }) => {
 9 |   const url = `https://demo.1fe.com/playground?widgetUrl=https://1fe-a.akamaihd.net/integration/widgets/${packageJson.name}/${version}/js/1fe-bundle.js&fixPreview=true`;
10 |   await page.goto(url);
11 | 
12 |   await expect(page.getByText("Welcome to 1FE Starter Kit")).toBeVisible();
13 | });
14 | 


--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
 1 | import { configureStore } from "@reduxjs/toolkit";
 2 | 
 3 | import helloReducer from "./slices/hello";
 4 | 
 5 | /**
 6 |  * Creates a Redux store that's isolated to this widget
 7 |  */
 8 | export const createStore = () => {
 9 |   const store = configureStore({
10 |     reducer: {
11 |       hello: helloReducer,
12 |     },
13 |   });
14 | 
15 |   return store;
16 | };
17 | 
18 | // Export types for the store, root state, and dispatch
19 | export type Store = ReturnType;
20 | export type RootState = ReturnType;
21 | export type AppDispatch = Store["dispatch"];
22 | 


--------------------------------------------------------------------------------
/src/withProvider.tsx:
--------------------------------------------------------------------------------
 1 | import React, { useState } from "react";
 2 | import { Provider } from "react-redux";
 3 | import { createStore } from "./store";
 4 | import { WidgetProps } from "./contract";
 5 | 
 6 | /**
 7 |  * Wrap the application code in the various app level providers
 8 |  */
 9 | export const withProvider = (Component: React.FC) =>
10 |   function WidgetProvider(props: WidgetProps): React.ReactElement {
11 |     // Create a new store for each instance of the widget
12 |     // Automatically is destroyed when the widget is unmounted
13 |     const [store] = useState(createStore);
14 | 
15 |     return (
16 |       
17 |         
18 |       
19 |     );
20 |   };
21 | 


--------------------------------------------------------------------------------
/src/declarations.d.ts:
--------------------------------------------------------------------------------
 1 | declare module "*.html" {
 2 |   const rawHtmlFile: string;
 3 |   export = rawHtmlFile;
 4 | }
 5 | 
 6 | declare module "*.bmp" {
 7 |   const src: string;
 8 |   export default src;
 9 | }
10 | 
11 | declare module "*.gif" {
12 |   const src: string;
13 |   export default src;
14 | }
15 | 
16 | declare module "*.jpg" {
17 |   const src: string;
18 |   export default src;
19 | }
20 | 
21 | declare module "*.jpeg" {
22 |   const src: string;
23 |   export default src;
24 | }
25 | 
26 | declare module "*.png" {
27 |   const src: string;
28 |   export default src;
29 | }
30 | 
31 | declare module "*.webp" {
32 |   const src: string;
33 |   export default src;
34 | }
35 | 
36 | declare module "*.svg" {
37 |   const src: string;
38 |   export default src;
39 | }
40 | 


--------------------------------------------------------------------------------
/src/components/experience/index.tsx:
--------------------------------------------------------------------------------
 1 | import { Flex, Card } from "antd";
 2 | 
 3 | import { GetTitle } from "./getTitle";
 4 | import { SetTitle } from "./setTitle";
 5 | import { UtilitySection } from "../common/UtilitySection";
 6 | import { utilityCard } from "../../sharedStyles";
 7 | 
 8 | /**
 9 |  * Experience Utility examples
10 |  */
11 | export const Experience: React.FC = () => {
12 |   return (
13 |     
14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/components/routes/styles/utilsDemo.styles.ts: -------------------------------------------------------------------------------- 1 | export const heroTitle = { 2 | margin: 0, 3 | marginBottom: 16, 4 | background: "linear-gradient(45deg, #1890ff, #722ed1)", 5 | WebkitBackgroundClip: "text", 6 | WebkitTextFillColor: "transparent", 7 | fontSize: "2.5rem", 8 | }; 9 | 10 | export const heroDescription = { 11 | fontSize: 16, 12 | maxWidth: 800, 13 | margin: "0 auto", 14 | lineHeight: 1.6, 15 | }; 16 | 17 | export const rocketIcon = { 18 | marginRight: 12, 19 | }; 20 | 21 | export const infoCard = { 22 | marginTop: 20, 23 | maxWidth: 600, 24 | margin: "20px auto 0", 25 | backgroundColor: "#f0f9ff", 26 | border: "1px solid #bae7ff", 27 | }; 28 | 29 | export const infoIconContainer = { 30 | display: "flex", 31 | alignItems: "center", 32 | justifyContent: "center", 33 | gap: 8, 34 | }; 35 | 36 | export const infoText = { 37 | fontSize: 14, 38 | fontWeight: 500, 39 | }; 40 | -------------------------------------------------------------------------------- /.github/workflows/widget-ci.yml: -------------------------------------------------------------------------------- 1 | name: Widget CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | inputs: 12 | node_version_override: 13 | description: "Override Node.js version for this run (e.g., 20)" 14 | required: false 15 | type: string 16 | 17 | permissions: 18 | contents: write 19 | pull-requests: write 20 | issues: write 21 | repository-projects: write 22 | id-token: write 23 | 24 | jobs: 25 | run_minimal_ci: 26 | name: Widget CI 27 | uses: docusign/1fe-ci-cd/.github/workflows/ci-widgets.yml@main 28 | with: 29 | node-version: ${{ github.event.inputs.node_version_override || '22' }} 30 | secrets: 31 | AKAMAI_NS_SSH_PRIVATE_KEY: ${{ secrets.AKAMAI_NS_SSH_PRIVATE_KEY }} 32 | AZURE_APP_CONFIG_CONNECTION_STRING: ${{ secrets.AZURE_APP_CONFIG_CONNECTION_STRING }} 33 | PRIVATE_KEY_1FE_ADMIN_GITHUB_APP: ${{ secrets.PRIVATE_KEY_1FE_ADMIN_GITHUB_APP }} 34 | -------------------------------------------------------------------------------- /.github/workflows/rollback-widget.yml: -------------------------------------------------------------------------------- 1 | name: Rollback Widget 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | target-version: 7 | description: "The specific version to roll back to (e.g., 1.0.3 or 1.0.3-PR-123.abcde12)" 8 | required: true 9 | type: string 10 | environment: 11 | description: "The environment to rollback (e.g., integration or production)" 12 | required: true 13 | type: choice 14 | options: 15 | - integration 16 | - production 17 | 18 | permissions: 19 | contents: read 20 | id-token: write 21 | 22 | jobs: 23 | rollback-widget: 24 | uses: docusign/1fe-ci-cd/.github/workflows/reusable-rollback-ci.yml@main 25 | with: 26 | target-version: ${{ github.event.inputs.target-version }} 27 | environment: ${{ github.event.inputs.environment }} 28 | caller-repo: ${{ github.repository }} 29 | caller-repo-ref: ${{ github.ref }} 30 | secrets: 31 | AZURE_APP_CONFIG_CONNECTION_STRING: ${{ secrets.AZURE_APP_CONFIG_CONNECTION_STRING }} 32 | -------------------------------------------------------------------------------- /src/components/common/UtilitySection.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tooltip } from "antd"; 3 | import { InfoCircleOutlined } from "@ant-design/icons"; 4 | import { colors } from "../../sharedStyles"; 5 | import { 6 | section, 7 | header, 8 | title as titleStyle, 9 | helpIcon, 10 | description as descriptionStyle, 11 | } from "./styles/UtilitySection.styles"; 12 | 13 | interface UtilitySectionProps { 14 | title: string; 15 | description: string; 16 | children?: React.ReactNode; 17 | } 18 | 19 | export const UtilitySection: React.FC = ({ 20 | title, 21 | description, 22 | children, 23 | }) => { 24 | return ( 25 |
26 |
27 |

{title}

28 | 29 | 30 | 31 |
32 |

33 | {description} 34 |

35 | {children} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Docusign Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/common/UtilityTooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tooltip, Button, ButtonProps } from "antd"; 3 | import { 4 | tooltipContent, 5 | tooltipTitle, 6 | tooltipDescription, 7 | tooltipApi, 8 | } from "./styles/UtilityTooltip.styles"; 9 | 10 | interface UtilityTooltipProps extends ButtonProps { 11 | title: string; 12 | description: string; 13 | apiMethod: string; 14 | children?: React.ReactNode; 15 | placement?: "top" | "bottom" | "left" | "right"; 16 | } 17 | 18 | export const UtilityTooltip: React.FC = ({ 19 | title, 20 | description, 21 | apiMethod, 22 | children, 23 | placement = "top", 24 | ...buttonProps 25 | }) => { 26 | const tooltipContentElement = ( 27 |
28 |
{title}
29 |
{description}
30 |
API: {apiMethod}
31 |
32 | ); 33 | 34 | return ( 35 | 36 | 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/store/api/base.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BaseQueryFn, 3 | FetchArgs, 4 | FetchBaseQueryError, 5 | createApi, 6 | fetchBaseQuery, 7 | retry, 8 | } from "@reduxjs/toolkit/query/react"; 9 | 10 | const REDUCER_PATH = "widgetStarterKitApi"; 11 | 12 | export const determineEndpoint = (/*baseUrl?: string*/): string => { 13 | // eventually we will use the 1FE helper function for this 14 | // e.g. platform.network.getESignProxyUrlForESignEnvironment(baseUrl); 15 | // see proposed structure here {{ FILL IN }} 16 | return `/api/proxy/`; 17 | }; 18 | 19 | const enhancedBaseQuery: BaseQueryFn< 20 | string | FetchArgs, 21 | unknown, 22 | FetchBaseQueryError 23 | > = async (args, api, extraOptions) => { 24 | return retry( 25 | fetchBaseQuery({ 26 | baseUrl: determineEndpoint(), 27 | timeout: 60000, 28 | prepareHeaders: (headers) => { 29 | headers.set("Content-Type", "application/json"); 30 | return headers; 31 | }, 32 | }), 33 | { maxRetries: 3 }, 34 | )(args, api, extraOptions); 35 | }; 36 | 37 | export const baseApi = createApi({ 38 | baseQuery: enhancedBaseQuery, 39 | keepUnusedDataFor: 30, 40 | endpoints: () => ({}), 41 | reducerPath: REDUCER_PATH, 42 | refetchOnReconnect: true, 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/routes/styles/home.styles.ts: -------------------------------------------------------------------------------- 1 | export const heroTitle = { 2 | margin: 0, 3 | marginBottom: 16, 4 | background: "linear-gradient(45deg, #1890ff, #722ed1)", 5 | WebkitBackgroundClip: "text", 6 | WebkitTextFillColor: "transparent", 7 | fontSize: "2.5rem", 8 | }; 9 | 10 | export const heroDescription = { 11 | fontSize: 16, 12 | maxWidth: 800, 13 | margin: "0 auto", 14 | lineHeight: 1.6, 15 | }; 16 | 17 | export const rocketIcon = { 18 | marginRight: 12, 19 | }; 20 | 21 | export const panelHeader = { 22 | display: "flex", 23 | alignItems: "center", 24 | gap: 8, 25 | }; 26 | 27 | export const panelDescription = { 28 | fontSize: 14, 29 | }; 30 | 31 | export const jsonViewerContainer = { 32 | backgroundColor: "#fafafa", 33 | padding: 16, 34 | borderRadius: 6, 35 | maxHeight: 400, 36 | overflow: "auto", 37 | }; 38 | 39 | export const proTipBox = { 40 | marginTop: 16, 41 | padding: 12, 42 | backgroundColor: "#fff7e6", 43 | borderRadius: 6, 44 | fontSize: 14, 45 | color: "#fa8c16", 46 | }; 47 | 48 | export const codeSnippet = { 49 | backgroundColor: "#f5f5f5", 50 | padding: "2px 6px", 51 | borderRadius: 4, 52 | fontFamily: "monospace", 53 | }; 54 | 55 | export const introSection = { 56 | marginBottom: 16, 57 | }; 58 | 59 | export const introText = { 60 | margin: 0, 61 | }; 62 | 63 | export const jsonViewerStyle = { 64 | fontSize: 12, 65 | }; 66 | -------------------------------------------------------------------------------- /src/components/experience/setTitle.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import { Input } from "antd"; 3 | import { useState } from "react"; 4 | import { EditOutlined } from "@ant-design/icons"; 5 | 6 | import { UtilityTooltip } from "../common/UtilityTooltip"; 7 | 8 | export const SetTitle = () => { 9 | const [title, setTitle] = useState("Hello World"); 10 | 11 | return ( 12 |
22 | setTitle(e.target.value)} 25 | value={title} 26 | placeholder="Enter new page title" 27 | style={{ flex: 1 }} 28 | prefix={} 29 | /> 30 | platformProps.utils.experience.title.set(title)} 37 | > 38 | Set Title 39 | 40 |
41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = /** @type {import("eslint").Linter.Config} */ { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended", 7 | "eslint-config-prettier", 8 | ], 9 | parser: "@typescript-eslint/parser", // Use the TypeScript parser 10 | parserOptions: { 11 | ecmaVersion: 2022, // Support the latest ECMAScript features 12 | sourceType: "module", // Enable ECMAScript modules 13 | ecmaFeatures: { 14 | jsx: true, 15 | }, 16 | }, 17 | plugins: ["@typescript-eslint", "prettier"], 18 | env: { 19 | browser: true, // Enable browser global variables 20 | node: true, // Enable Node.js global variables 21 | es6: true, // Enable ECMAScript 6 features 22 | }, 23 | settings: { 24 | react: { 25 | version: "detect", // Automatically detect the React version 26 | }, 27 | }, 28 | rules: { 29 | "prettier/prettier": "error", // Treat Prettier violations as errors 30 | "@typescript-eslint/explicit-module-boundary-types": "off", // Optional: Disables enforcing explicit return types on function signatures 31 | "react/no-unknown-property": ["error", { ignore: ["css"] }], // Required for emotion css prop 32 | "react/no-unescaped-entities": "off", // Allow apostrophes and quotes in JSX 33 | "react/react-in-jsx-scope": "off", 34 | "react/jsx-uses-react": "off", 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/experience/getTitle.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import { useState } from "react"; 3 | import { EyeOutlined } from "@ant-design/icons"; 4 | 5 | import { UtilityTooltip } from "../common/UtilityTooltip"; 6 | 7 | export const GetTitle = () => { 8 | const [currentTitle, setCurrentTitle] = useState(""); 9 | 10 | const handleGetTitle = () => { 11 | const title = platformProps.utils.experience.title.get(); 12 | setCurrentTitle(title); 13 | }; 14 | 15 | return ( 16 |
26 | 34 | Get Current Title 35 | 36 | {currentTitle && ( 37 |
47 | Title: "{currentTitle}" 48 |
49 | )} 50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # generateds files 30 | reports 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # ts-import cache 52 | .cache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | dist 69 | 70 | # Editor directories and files 71 | .idea 72 | .vscode 73 | *.suo 74 | *.ntvs* 75 | *.njsproj 76 | *.sln 77 | *.sw? 78 | .DS_Store 79 | 80 | # transient types 81 | src/types/widgets 82 | 83 | # Yarn 84 | .pnp.* 85 | .yarn/* 86 | !.yarn/patches 87 | !.yarn/plugins 88 | !.yarn/releases 89 | !.yarn/sdks 90 | !.yarn/versions 91 | -------------------------------------------------------------------------------- /src/components/router.tsx: -------------------------------------------------------------------------------- 1 | import { Layout, Menu } from "antd"; 2 | import { Route, Routes, useNavigate } from "react-router-dom"; 3 | 4 | import { Home } from "./routes/home"; 5 | import { UtilsDemo } from "./routes/utilsDemo"; 6 | import { WidgetProps } from "../contract"; 7 | import OneFeLogo from "../assets/1fe-logo.svg"; 8 | 9 | /** 10 | * Suggestion: setup the top level routing for your application here. 11 | */ 12 | export const Router: React.FC = (props) => { 13 | const navigate = useNavigate(); 14 | 15 | const { Header } = Layout; 16 | 17 | return ( 18 | 19 |
30 | 1FE Logo 31 | navigate(""), // favor navigating with relative route (for bathtub compatability) 40 | }, 41 | { 42 | key: "UTILS", 43 | label: "Utils", 44 | onClick: () => navigate("utils"), // favor navigating with relative route (for bathtub compatability) 45 | }, 46 | ]} 47 | style={{ 48 | flex: 1, 49 | minWidth: 0, 50 | }} 51 | /> 52 |
53 | 54 | } /> 55 | } /> 56 | {"404"}} /> 57 | 58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /src/components/misc/utils.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import React from "react"; 3 | 4 | import { SkeletonLoader } from "./skeletonLoader"; 5 | 6 | const INTERNAL_GENERIC_CHILD = "@1fe/sample-widget"; 7 | // const INTERNAL_GENERIC_VARIANTS = "@internal/generic-variants-widget"; 8 | 9 | export const GetChildWidget: React.FC<{ 10 | isVisible: boolean; 11 | widgetId?: string; 12 | }> = ({ isVisible, widgetId = INTERNAL_GENERIC_CHILD }) => { 13 | const Widget = platformProps.utils.widgets.get(widgetId); 14 | // const Widget = platformProps?.utils.widgets.get(widgetId); 15 | 16 | return isVisible ? : null; 17 | }; 18 | 19 | export const GetChildWidgetWithCustomLoader: React.FC<{ 20 | isVisible: boolean; 21 | widgetId?: string; 22 | }> = ({ isVisible, widgetId = INTERNAL_GENERIC_CHILD }) => { 23 | const Widget = platformProps.utils.widgets.get(widgetId, { 24 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 25 | // @ts-ignore - Remove when the Loader option is available in widgets.get() from @1fe/shell 26 | Loader: ( 27 |
28 | 29 |
30 | ), 31 | }); 32 | 33 | // const Widget = platformProps?.utils.widgets.get(widgetId, { 34 | // // eslint-disable-next-line @typescript-eslint/ban-ts-comment 35 | // // @ts-ignore - Remove when the Loader option is available in widgets.get() from @1fe/shell 36 | // Loader: ( 37 | //
38 | // 39 | //
40 | // ), 41 | // }); 42 | 43 | return isVisible ? : null; 44 | }; 45 | 46 | // export const GetVariant: React.FC<{ 47 | // isVisible: boolean; 48 | // widgetId?: string; 49 | // variantId?: string; 50 | // }> = ({ 51 | // isVisible, 52 | // widgetId = INTERNAL_GENERIC_VARIANTS, 53 | // variantId = 'testVariant', 54 | // }) => { 55 | // const Variant = platformProps?.utils.widgets.get(widgetId, { variantId }); 56 | 57 | // return isVisible ? : null; 58 | // }; 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@1fe/widget-starter-kit", 3 | "version": "0.1.9", 4 | "license": "MIT", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "1fe-cli --trace --environment integration dev", 8 | "build:widget": "1fe-cli --trace --environment integration build", 9 | "lint": "eslint . --report-unused-disable-directives --max-warnings 0 && echo ✔ Lint passed!", 10 | "lint:fix": "eslint . --fix && echo ✔ Lint errors auto-fixed!", 11 | "format": "prettier --write .", 12 | "check-format": "prettier --check .", 13 | "prepack": "yarn build", 14 | "test": "yarn jest" 15 | }, 16 | "devDependencies": { 17 | "@1fe/cli": "^0.1.1", 18 | "@1fe/sample-widget-base-config": "^0.1.2", 19 | "@playwright/test": "^1.53.1", 20 | "@testing-library/dom": "^10.4.0", 21 | "@testing-library/jest-dom": "^6.6.3", 22 | "@testing-library/react": "^16.3.0", 23 | "@types/jest": "^29", 24 | "@types/react": "^18.2.62", 25 | "@types/react-dom": "^18.2.19", 26 | "@typescript-eslint/eslint-plugin": "^8.35.0", 27 | "@typescript-eslint/parser": "^8.35.0", 28 | "cross-env": "^7.0.3", 29 | "eslint": "8.57.1", 30 | "eslint-config-prettier": "^10.1.1", 31 | "eslint-config-ts-react-important-stuff": "^3.0.0", 32 | "eslint-plugin-prettier": "^5.2.4", 33 | "eslint-plugin-react": "^7.37.5", 34 | "globals": "^16.3.0", 35 | "identity-obj-proxy": "^3.0.0", 36 | "jest": "^29", 37 | "jest-environment-jsdom": "^29", 38 | "prettier": "^3.5.3", 39 | "pretty-quick": "^3.1.1", 40 | "ts-config-single-spa": "^3.0.0", 41 | "ts-jest": "^29", 42 | "typescript": "5.8.2" 43 | }, 44 | "dependencies": { 45 | "@1fe/shell": "^0.1.1", 46 | "@emotion/react": "^11.14.0", 47 | "@emotion/styled": "^11.14.0", 48 | "@mui/material": "^7.1.0", 49 | "@reduxjs/toolkit": "1.9.1", 50 | "@textea/json-viewer": "^4.0.1", 51 | "@types/systemjs": "^6.15.1", 52 | "antd": "^5.22.4", 53 | "react": "^18.3.1", 54 | "react-dom": "^18.3.1", 55 | "react-redux": "^8.1.3", 56 | "react-router-dom": "6.30.0", 57 | "reselect": "^4.1.7", 58 | "single-spa": "^5.9.3", 59 | "single-spa-react": "^6.0.2" 60 | }, 61 | "packageManager": "yarn@4.9.1" 62 | } 63 | -------------------------------------------------------------------------------- /src/components/platformPropsImport/comparePlatformProps.tsx: -------------------------------------------------------------------------------- 1 | // import { platformProps } from '@1fe/shell'; 2 | // import { Button } from 'antd'; 3 | // import { isEqual } from 'lodash'; 4 | // import { useState } from 'react'; 5 | 6 | // import { WidgetProps } from 'src/contract'; 7 | // import { ResultElementBoundary } from '../misc/resultElementBoundary'; 8 | // import { useTranslate } from 'src/locales'; 9 | 10 | /** 11 | * JSON stringify helper that prevents circular references 12 | * and outputs functions and symbols 13 | * @returns string value that represents the current key 14 | */ 15 | export const jsonReplacer = () => { 16 | const visited = new WeakSet(); 17 | return (_key: unknown, value: unknown) => { 18 | if (typeof value === "object" && value !== null) { 19 | if (visited.has(value)) { 20 | return; 21 | } 22 | visited.add(value); 23 | } 24 | if (typeof value == "function") { 25 | return "function"; 26 | } 27 | if (typeof value == "symbol") { 28 | return "symbol"; 29 | } 30 | if (typeof value === "undefined") { 31 | return null; 32 | } 33 | return value; 34 | }; 35 | }; 36 | 37 | // TODO: strongly type 38 | // export const GetComparePlatformProps: React.FC = (props) => { 39 | // // const t = useTranslate(); 40 | // const [result, setResult] = useState(''); 41 | 42 | // return ( 43 | //
44 | // 63 | //
64 | // {result ? ( 65 | // 68 | // {result} 69 | // 70 | // ) : null} 71 | //
72 | //
73 | // ); 74 | // }; 75 | -------------------------------------------------------------------------------- /src/assets/1fe-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/routes/utilsDemo.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { Flex, Typography, Card } from "antd"; 3 | import { InfoCircleOutlined, RocketOutlined } from "@ant-design/icons"; 4 | 5 | import { Context } from "../context/context"; 6 | import { EventBus } from "../eventBus/eventBus"; 7 | import { Experience } from "../experience"; 8 | import { LocalAndSessionStorage } from "../localAndSessionStorage"; 9 | import { Widgets } from "../widgets"; 10 | 11 | import { AppLoadTime } from "../appLoadTime/appLoadTime"; 12 | import { CustomLoader } from "../customLogger"; 13 | import { colors } from "../../sharedStyles"; 14 | import { 15 | heroTitle, 16 | heroDescription, 17 | rocketIcon, 18 | infoCard, 19 | infoIconContainer, 20 | infoText, 21 | } from "./styles/utilsDemo.styles"; 22 | 23 | const Container = styled.div({ 24 | padding: "32px", 25 | backgroundColor: "#fafafa", 26 | minHeight: "100vh", 27 | }); 28 | 29 | const HeaderSection = styled.div({ 30 | textAlign: "center", 31 | marginBottom: "48px", 32 | padding: "32px", 33 | backgroundColor: "white", 34 | borderRadius: "12px", 35 | boxShadow: "0 2px 8px rgba(0,0,0,0.1)", 36 | }); 37 | 38 | const { Title, Paragraph } = Typography; 39 | 40 | export const UtilsDemo = () => { 41 | return ( 42 | 43 | 44 | 45 | <RocketOutlined style={{ ...rocketIcon, color: colors.primary }} /> 46 | 1FE Platform Utilities 47 | 48 | 49 | This page highlights the default platform utilities available to 1FE 50 | widgets. Each utility is designed to solve common widget development 51 | challenges - from managing context and storage to inter-widget 52 | communication and performance monitoring. It also highlights a custom 53 | utility that was added by the 1FE instance and is being made available 54 | by the shell. 55 | 56 | 57 |
58 | 59 | 60 | Hover over any button to see detailed explanations and API usage 61 | 62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /src/components/context/context.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import { Flex, Card } from "antd"; 3 | import { useState } from "react"; 4 | import { 5 | SettingOutlined, 6 | InfoCircleOutlined, 7 | GlobalOutlined, 8 | } from "@ant-design/icons"; 9 | 10 | import { GetChildWidget } from "../misc/utils"; 11 | import { WidgetContainer } from "../misc/widgetContainer"; 12 | import { UtilityTooltip } from "../common/UtilityTooltip"; 13 | import { UtilitySection } from "../common/UtilitySection"; 14 | import { utilityCard, flexProps } from "../../sharedStyles"; 15 | 16 | export const Context = () => { 17 | const [showWidget, setShowWidget] = useState(false); 18 | const [contextResult, setContextResult] = useState(""); 19 | 20 | return ( 21 |
22 | 26 | 27 | 28 | setShowWidget(true)} 36 | > 37 | Load Child Widget 38 | 39 | 40 | { 47 | setShowWidget(false); 48 | setContextResult( 49 | JSON.stringify(platformProps.context.self, null, 2), 50 | ); 51 | }} 52 | > 53 | Get My Context 54 | 55 | 56 | { 63 | setShowWidget(false); 64 | setContextResult( 65 | JSON.stringify(platformProps.context.getHost(), null, 2), 66 | ); 67 | }} 68 | > 69 | Get Host's Context 70 | 71 | 72 | 73 | {showWidget ? ( 74 | 75 | 76 | 77 | ) : ( 78 |
91 | {contextResult || 92 | "Click a button above to see context information"} 93 |
94 | )} 95 |
96 |
97 |
98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/components/customLogger/index.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Card } from "antd"; 2 | import { useState } from "react"; 3 | import { BugOutlined, InfoCircleOutlined } from "@ant-design/icons"; 4 | import { platformProps } from "@1fe/shell"; 5 | 6 | import { UtilityTooltip } from "../common/UtilityTooltip"; 7 | import { UtilitySection } from "../common/UtilitySection"; 8 | import { utilityCard, flexProps } from "../../sharedStyles"; 9 | 10 | /** 11 | * Platform Logger Utilities - Demonstrates 1FE platform logging 12 | */ 13 | export const CustomLoader: React.FC = () => { 14 | const [logState, setLogState] = useState(false); 15 | 16 | const logInfo = (message: string) => { 17 | // Use platform logger utility if available, fallback to console 18 | if ( 19 | ( 20 | platformProps.utils as unknown as { 21 | logger?: { log: (msg: string) => void }; 22 | } 23 | )?.logger?.log 24 | ) { 25 | // @ts-expect-error - logger types are not fully defined in platform yet 26 | platformProps.utils.logger.log(message); 27 | 28 | setLogState(true); 29 | } 30 | }; 31 | 32 | const logError = (message: string) => { 33 | // Use platform logger utility if available, fallback to console 34 | if ( 35 | ( 36 | platformProps.utils as unknown as { 37 | logger?: { error: (msg: string) => void }; 38 | } 39 | )?.logger?.error 40 | ) { 41 | // @ts-expect-error - logger types are not fully defined in platform yet 42 | platformProps.utils.logger.error(message); 43 | 44 | setLogState(true); 45 | } 46 | }; 47 | 48 | return ( 49 |
50 | 54 | 55 | 56 | 63 | logInfo( 64 | "This is an example info message using platform logger utilities!", 65 | ) 66 | } 67 | > 68 | Log Info 69 | 70 | 71 | 78 | logError( 79 | "This is an example error message using platform logger utilities!", 80 | ) 81 | } 82 | > 83 | Log Error 84 | 85 | 86 | 87 | {logState && ( 88 |
89 |
97 | 💡 Check your browser's developer console (F12) to see platform 98 | logger output and telemetry data 99 |
100 |
101 | )} 102 |
103 |
104 |
105 | ); 106 | }; 107 | -------------------------------------------------------------------------------- /src/components/widgets/get.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, Card } from "antd"; 2 | import { useState } from "react"; 3 | import { AppstoreOutlined, LoadingOutlined } from "@ant-design/icons"; 4 | 5 | import { GetChildWidget, GetChildWidgetWithCustomLoader } from "../misc/utils"; 6 | import { WidgetContainer } from "../misc/widgetContainer"; 7 | import { UtilityTooltip } from "../common/UtilityTooltip"; 8 | import { UtilitySection } from "../common/UtilitySection"; 9 | import { utilityCard, flexProps } from "../../sharedStyles"; 10 | 11 | export const Get = () => { 12 | const [showWidget, setShowWidget] = useState(false); 13 | const [ 14 | showWidgetWithCustomLoaderVisible, 15 | setShowWidgetWithCustomLoaderVisible, 16 | ] = useState(false); 17 | 18 | return ( 19 |
20 | 24 | 25 | 26 | { 33 | setShowWidget(true); 34 | setShowWidgetWithCustomLoaderVisible(false); 35 | }} 36 | > 37 | Load Widget 38 | 39 | 40 | { 47 | setShowWidgetWithCustomLoaderVisible(true); 48 | setShowWidget(false); 49 | }} 50 | > 51 | Load with Custom Loader 52 | 53 | 54 | 55 | {showWidget && ( 56 |
57 |
67 | 🎯 Standard widget loaded with default platform loader 68 |
69 | 70 | 71 | 72 |
73 | )} 74 | 75 | {showWidgetWithCustomLoaderVisible && ( 76 |
77 |
87 | ⚡ Widget loaded with custom loading indicator 88 |
89 | 90 | 93 | 94 |
95 | )} 96 |
97 |
98 |
99 | ); 100 | }; 101 | -------------------------------------------------------------------------------- /src/components/eventBus/eventBus.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import { Flex, Card } from "antd"; 3 | import { useReducer } from "react"; 4 | import { SendOutlined, MessageOutlined } from "@ant-design/icons"; 5 | 6 | import { GetChildWidget } from "../misc/utils"; 7 | import { WidgetContainer } from "../misc/widgetContainer"; 8 | import { UtilityTooltip } from "../common/UtilityTooltip"; 9 | import { UtilitySection } from "../common/UtilitySection"; 10 | import { utilityCard, flexProps } from "../../sharedStyles"; 11 | 12 | export const EventBus = () => { 13 | const [isVisible, showWidget] = useReducer(() => true, false); 14 | 15 | type WidgetEvents = { 16 | event1: { param1: string }; 17 | event2: { param2: string }; 18 | }; 19 | 20 | return ( 21 |
22 | 26 | 27 | 28 | 36 | Load Event Listener 37 | 38 | 39 | { 46 | platformProps.utils.eventBus.publish({ 47 | targetWidgetId: "@1fe/sample-widget", 48 | eventName: "event1", 49 | data: { param1: "Listener is working!" }, 50 | }); 51 | }} 52 | > 53 | Send a published event 54 | 55 | 56 | { 63 | platformProps.utils.eventBus.publish({ 64 | targetWidgetId: "@internal/generic-child-widget", 65 | eventName: "event2", 66 | data: { param2: "Test 2 should not fire" }, 67 | }); 68 | }} 69 | > 70 | Send an unpublished event 71 | 72 | 73 | 74 | {isVisible && ( 75 |
76 |
86 | 📡 Event listener widget loaded below. Try sending events using 87 | the buttons above! 88 |
89 | 90 | 94 | 95 |
96 | )} 97 |
98 |
99 |
100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1fe Widget Starter Kit 2 | 3 | A template for building widgets within the 1fe ecosystem. This starter kit provides the foundation, examples, and best practices for developing production-ready widgets. 4 | 5 | It powers the starter-kit plugin on demo.1fe.com() 6 | 7 | ## What's in this repository 8 | 9 | This starter kit provides: 10 | 11 | - **Widget development setup** with TypeScript, React, and 1fe CLI integration 12 | - **Example components** demonstrating common widget patterns and platform utilities 13 | - **Testing framework** with Jest for unit tests and Playwright for E2E tests 14 | - **Development tools** including ESLint, Prettier, and hot reloading 15 | - **State management** using Redux Toolkit 16 | - **UI components** with Material-UI and Antd examples 17 | 18 | ## Prerequisites 19 | 20 | - **Node.js** `>= 22` 21 | - **Yarn** (package manager) 22 | 23 | ## Getting Started 24 | 25 | ### Development Setup 26 | 27 | Your organization may have a template which is based off of this repository. IF so, use that for building your widgets and plugins instead. 28 | 29 | Following steps will get you started hosting this plugin 30 | 31 | ```bash 32 | # Clone this repository 33 | git clone 34 | cd 1fe-widget-starter-kit 35 | 36 | # Install dependencies 37 | yarn install 38 | 39 | # Start development server 40 | yarn dev 41 | ``` 42 | 43 | ### Project Structure 44 | 45 | ```text 46 | src/ 47 | ├── components/ 48 | │ ├── appLoadTime/ # Performance monitoring examples 49 | │ ├── context/ # React context examples 50 | │ ├── customLogger/ # Logging utilities 51 | │ ├── eventBus/ # Event communication examples 52 | │ ├── experience/ # User experience components 53 | │ ├── localAndSessionStorage/ # Storage utilities 54 | │ ├── misc/ # Miscellaneous utilities 55 | │ ├── platformPropsImport/ # Platform integration examples 56 | │ ├── routes/ # Routing examples 57 | │ └── widgets/ # Widget composition examples 58 | ├── store/ # Redux store configuration 59 | ├── types/ # TypeScript type definitions 60 | ├── assets/ # Static assets 61 | ├── app1.tsx # Main application component 62 | ├── contract.ts # Widget contract definition 63 | ├── root.component.tsx # Root component 64 | └── widget.ts # Widget entry point 65 | ``` 66 | 67 | ## Development Commands 68 | 69 | ```bash 70 | # Start development server with hot reloading 71 | yarn dev 72 | 73 | # Build widget for production 74 | yarn build:widget 75 | 76 | # Run unit tests 77 | yarn test 78 | # Lint code 79 | yarn lint 80 | 81 | # Format code 82 | yarn format 83 | 84 | # Check code formatting 85 | yarn check-format 86 | ``` 87 | 88 | ## Widget Configuration 89 | 90 | ### 1fe Configuration 91 | 92 | The `.1fe.config.ts` file defines widget configuration: 93 | 94 | ```typescript 95 | import { OneFeConfiguration } from "@1fe/cli"; 96 | import { getBaseConfig } from "@1fe/sample-widget-base-config"; 97 | 98 | const configuration: OneFeConfiguration = { 99 | baseConfig: getBaseConfig, 100 | }; 101 | ``` 102 | 103 | ### Widget Contract 104 | 105 | Define your widget's interface in `src/contract.ts`: 106 | 107 | ```typescript 108 | export interface MyWidgetContract { 109 | // Define your widget's props and methods 110 | } 111 | ``` 112 | 113 | ## Example Components 114 | 115 | The starter kit includes examples for: 116 | 117 | - **App Load Time**: Performance monitoring and metrics 118 | - **Context**: React context for state sharing 119 | - **Custom Logger**: Logging integration with platform 120 | - **Event Bus**: Inter-widget communication 121 | - **Local/Session Storage**: Data persistence utilities 122 | - **Platform Props**: Accessing 1fe platform utilities 123 | - **Router**: Navigation and routing patterns 124 | - **Widgets**: Composing multiple widget components 125 | 126 | ## Testing 127 | 128 | ### Unit Testing 129 | 130 | - Uses Jest with React Testing Library 131 | - Test files: `*.test.tsx` or `*.spec.tsx` 132 | - Run with: `yarn test` 133 | 134 | ### E2E Testing 135 | 136 | - Uses Playwright for browser testing 137 | - Test files in `tests/` directory 138 | 139 | ## Building for Production 140 | 141 | ```bash 142 | # Build optimized widget bundle 143 | yarn build:widget 144 | 145 | # The built files will be in the dist/ directory 146 | ``` 147 | 148 | ## Contributing 149 | 150 | ### Development Workflow 151 | 152 | 1. Fork the repository 153 | 2. Create a feature branch (`git checkout -b feature/your-feature`) 154 | 3. Make your changes 155 | 4. Run tests (`yarn test`) 156 | 5. Ensure linting passes (`yarn lint`) 157 | 6. Format your code (`yarn format`) 158 | 7. Commit your changes (`git commit -m 'Add feature'`) 159 | 8. Push to your branch (`git push origin feature/your-feature`) 160 | 9. Open a Pull Request 161 | 162 | ### Code Quality 163 | 164 | - Follow TypeScript best practices 165 | - Maintain test coverage 166 | - Use provided ESLint and Prettier configurations 167 | - Document any new components or utilities 168 | 169 | ## Troubleshooting 170 | 171 | ### Common Issues 172 | 173 | - **Build failures**: Ensure Node.js version is >= 22 and dependencies are installed 174 | - **TypeScript errors**: Check type definitions in `src/types/` 175 | - **Test failures**: Verify mock configurations and test setup 176 | 177 | ## Getting Help 178 | 179 | - **[1fe Documentation](https://1fe.com/getting-started/installation/)** - Complete platform documentation 180 | - **[GitHub Issues](https://github.com/docusign/1fe/issues)** - Report bugs or request features 181 | - **[GitHub Discussions](https://github.com/docusign/1fe/discussions)** - Ask questions and share ideas 182 | 183 | ## License 184 | 185 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 186 | -------------------------------------------------------------------------------- /src/components/localAndSessionStorage/localAndSessionStorage1FE.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import { Flex, Card } from "antd"; 3 | import { useState } from "react"; 4 | import { DatabaseOutlined, HistoryOutlined } from "@ant-design/icons"; 5 | 6 | import { ResultElementBoundary } from "../misc/resultElementBoundary"; 7 | import { UtilityTooltip } from "../common/UtilityTooltip"; 8 | import { UtilitySection } from "../common/UtilitySection"; 9 | 10 | export const LocalAndSessionStorage1FE = () => { 11 | type allowedTypes = string | boolean | number; 12 | 13 | const [result, setResult] = useState(""); 14 | const [sessionResult, setSessionResult] = useState(); 15 | 16 | const getResult = (key: string, value: allowedTypes) => { 17 | platformProps.utils.localStorage.set(key, value); 18 | setResult(platformProps.utils.localStorage.get(key).toString()); 19 | }; 20 | 21 | const getSessionResult = (key: string, value: allowedTypes) => { 22 | platformProps.utils.sessionStorage.set(key, value); 23 | setSessionResult(platformProps.utils.sessionStorage.get(key).toString()); 24 | }; 25 | 26 | return ( 27 |
28 | 32 |
40 | 43 | 46 | Local Storage 47 | 48 | } 49 | style={{ flex: 1, minWidth: "350px" }} 50 | > 51 | 52 | 53 | getResult("key1", "value1")} 61 | > 62 | Save Text 63 | 64 | getResult("key2", true)} 71 | > 72 | Save Boolean 73 | 74 | 75 | 88 | {result || "No data stored yet"} 89 | 90 | 91 | 92 | 93 | 96 | 97 | Session Storage 98 | 99 | } 100 | style={{ flex: 1, minWidth: "350px" }} 101 | > 102 | 103 | 104 | getSessionResult("key1", "sessionStringValue")} 112 | > 113 | Save Session Text 114 | 115 | getSessionResult("key3", true)} 122 | > 123 | Save Session Boolean 124 | 125 | 126 | 139 | {sessionResult || "No session data stored yet"} 140 | 141 | 142 | 143 |
144 |
145 |
146 | ); 147 | }; 148 | -------------------------------------------------------------------------------- /src/components/appLoadTime/appLoadTime.tsx: -------------------------------------------------------------------------------- 1 | import { platformProps } from "@1fe/shell"; 2 | import { Flex, Card } from "antd"; 3 | import { useReducer, useState } from "react"; 4 | import { 5 | PlayCircleOutlined, 6 | StopOutlined, 7 | BarChartOutlined, 8 | } from "@ant-design/icons"; 9 | 10 | import { WidgetContainer } from "../misc/widgetContainer"; 11 | import { GetChildWidget } from "../misc/utils"; 12 | import { UtilityTooltip } from "../common/UtilityTooltip"; 13 | import { UtilitySection } from "../common/UtilitySection"; 14 | import { utilityCard, flexProps, colors } from "../../sharedStyles"; 15 | 16 | export const AppLoadTime = () => { 17 | const [isVisible, showWidget] = useReducer(() => true, false); 18 | const [entries, setEntries] = useState(null); 19 | const [measure, setMeasure] = useState(null); 20 | 21 | return ( 22 |
23 | 27 | 28 | 29 | 37 | Load Demo Widget 38 | 39 | 40 | { 47 | const resultString = platformProps.utils.appLoadTime 48 | .getEntries() 49 | .map((entry) => { 50 | return `Name: ${entry.name}, Type: ${entry.entryType}, Start: ${entry.startTime}ms, Duration: ${entry.duration}ms`; 51 | }) 52 | .join("; "); 53 | setEntries(resultString); 54 | }} 55 | > 56 | Get Performance Data 57 | 58 | 59 | { 66 | platformProps.utils.appLoadTime.markStart( 67 | "iLove1FESoMuchMarkTest", 68 | ); 69 | setMeasure( 70 | "⏱️ Timing started - click 'End Timing' to measure duration", 71 | ); 72 | }} 73 | > 74 | Start Timing 75 | 76 | 77 | { 84 | const result = platformProps.utils.appLoadTime.markEnd( 85 | "iLove1FESoMuchMarkTest", 86 | ); 87 | if (result) { 88 | setMeasure( 89 | `⏱️ ${result.name}: ${result.duration.toFixed(2)}ms`, 90 | ); 91 | } else { 92 | setMeasure( 93 | "❌ No timing started - click 'Start Timing' first", 94 | ); 95 | } 96 | }} 97 | > 98 | End Timing 99 | 100 | 101 | 102 | {entries && ( 103 |
104 |

105 | 📊 Performance Entries: 106 |

107 |
121 | {entries} 122 |
123 |
124 | )} 125 | 126 | {measure && ( 127 |
128 |

129 | ⏱️ Timing Result: 130 |

131 |
142 | {measure} 143 |
144 |
145 | )} 146 | 147 | {isVisible && ( 148 |
149 |

150 | 🎯 Demo Widget: 151 |

152 | 153 | 154 | 155 |
156 | )} 157 |
158 |
159 |
160 | ); 161 | }; 162 | -------------------------------------------------------------------------------- /src/components/routes/home.tsx: -------------------------------------------------------------------------------- 1 | import styled from "@emotion/styled"; 2 | import { Flex, Typography, Card, Collapse, Tag } from "antd"; 3 | import { RocketOutlined, ToolOutlined, BookOutlined } from "@ant-design/icons"; 4 | import React, { useState } from "react"; 5 | import { useSelector } from "react-redux"; 6 | import { useNavigate } from "react-router-dom"; 7 | import { JsonViewer } from "@textea/json-viewer"; 8 | 9 | import { RootState } from "../../store"; 10 | import { WidgetProps } from "../../contract"; 11 | import { UtilityTooltip } from "../common/UtilityTooltip"; 12 | import { UtilitySection } from "../common/UtilitySection"; 13 | import { utilityCard, flexProps, colors } from "../../sharedStyles"; 14 | import { 15 | heroTitle, 16 | heroDescription, 17 | rocketIcon, 18 | panelHeader, 19 | panelDescription, 20 | jsonViewerContainer, 21 | proTipBox, 22 | codeSnippet, 23 | introSection, 24 | introText, 25 | jsonViewerStyle, 26 | } from "./styles/home.styles"; 27 | 28 | const Container = styled.div({ 29 | padding: "32px", 30 | backgroundColor: "#fafafa", 31 | minHeight: "100vh", 32 | }); 33 | 34 | const HeaderSection = styled.div({ 35 | textAlign: "center", 36 | marginBottom: "48px", 37 | padding: "32px", 38 | backgroundColor: "white", 39 | borderRadius: "12px", 40 | boxShadow: "0 2px 8px rgba(0,0,0,0.1)", 41 | }); 42 | 43 | const { Title, Paragraph } = Typography; 44 | const { Panel } = Collapse; 45 | 46 | export const Home: React.FC = (props) => { 47 | const greeting = useSelector((state: RootState) => state.hello.greeting); 48 | const [expandedProps, setExpandedProps] = useState([ 49 | "platform", 50 | "host", 51 | ]); 52 | const navigate = useNavigate(); 53 | 54 | const propSections = [ 55 | { 56 | key: "platform", 57 | title: "Platform Properties", 58 | description: "Core platform utilities provided by 1FE", 59 | data: props.platform, 60 | color: "#1890ff", 61 | }, 62 | { 63 | key: "host", 64 | title: "Host Properties", 65 | description: 66 | "Properties passed from the host widget to this child widget.", 67 | data: props.host, 68 | color: "#52c41a", 69 | }, 70 | ]; 71 | 72 | return ( 73 | 74 | 75 | 76 | <RocketOutlined style={{ ...rocketIcon, color: colors.primary }} /> 77 | Welcome to 1FE Widget Starter Kit 78 | 79 | 80 | {greeting} You're now ready to build amazing widgets with the 1FE 81 | platform! This starter kit provides everything you need to create, 82 | test, and deploy widgets with modern development tools and best 83 | practices. 84 | 85 | 86 | 87 | 88 | {/* Getting Started Section */} 89 | 93 | 94 | 95 | navigate("utils")} 102 | > 103 | Explore Utils 104 | 105 | 106 | 113 | window.open("https://1fe.com/start-here/", "_blank") 114 | } 115 | > 116 | Explore Documentation 117 | 118 | 119 | 120 | 121 | 122 | {/* Widget Properties Section */} 123 | 127 | 128 | 129 |
130 | 133 | Your widget receives props from the platform and also from the 134 | host widget. Expand the sections below to explore what's 135 | available: 136 | 137 |
138 | 139 | 142 | setExpandedProps(Array.isArray(keys) ? keys : [keys]) 143 | } 144 | ghost 145 | > 146 | {propSections.map((section) => ( 147 | 150 | {section.title} 151 | 157 | {section.description} 158 | 159 | 160 | } 161 | key={section.key} 162 | > 163 |
164 | 171 |
172 |
173 | ))} 174 |
175 | 176 |
177 | 💡 Pro Tip: Use{" "} 178 | platformProps.utils.* to access 179 | powerful features like storage, context management, and 180 | inter-widget communication! 181 |
182 |
183 |
184 |
185 |
186 |
187 | ); 188 | }; 189 | --------------------------------------------------------------------------------