) => {
+ // do something...
+ };
+
+ // =========================================================================
+ // HELPER FUNCTIONS
+ // =========================================================================
+
+ /**
+ * If the helper function can be extracted (i.e. doesnt rely on any internal
+ * constant/state value), then consider putting it outside of the component
+ * instead
+ */
+
+ // =========================================================================
+ // RENDER FUNCTIONS
+ // =========================================================================
+ /**
+ * We recommend doing complex rendering in a render function
+ * of its own for ease of maintenance
+ */
+ const renderItems = () => {
+ // Map or complex render logic
+ };
+
+ /**
+ * Remember to pass down standard html element props to the inner component
+ * especially `className` to allow for external styling to be applied
+ */
+
+ return (
+
+ {renderItems()}
+
+ );
+};
+```
+
+## Prop specification
+
+Here are some guidelines on prop specification:
+
+- For callbacks, denote with `on` and the action. E.g. `onClick`, `onDismiss`
+- For optional props, use a Union type and add `undefined` to it
+
+```tsx
+interface MyComponentProps {
+ show?: boolean | undefined;
+ onChange?: ((param) => void) | undefined;
+}
+```
+
+- Avoid usage of enums to ease developer use. Opt for string literals instead
+ and do them in kebab-case
+
+```tsx
+interface MyComponentProps {
+ someType: "default" | "light" | "dark";
+}
+```
+
+- Make sure you specify common props like `id`, `className`, `data-testid`,
+ and `style` if you are not extending from standard HTML element props
+- Breakdown complex props into their own types too
+- Extend props whenever possible to avoid rewriting similar props
+- Ensure that public component types are uniquely named to avoid collision
+
+## Usage of useState
+
+We recommend that the use of state should be kept minimal unless it is meant for
+rendering purposes.
+
+## Styling practices
+
+### Use design tokens
+
+Avoid hardcoding CSS properties, use design tokens to get theming capabilities.
+
+Example:
+
+```tsx
+// Wrong
+const styledDiv = css`
+ font-size: 2.5rem;
+ font-weight: 100;
+ background: #edefef;
+`;
+
+// Correct
+const styledDiv = css`
+ ${Font["heading-xxl-light"]}
+ background: ${Colour["bg-strong"]};
+`;
+```
+
+### No `styled` helpers
+
+Do not use Linaria `styled` helpers to avoid accidental function interpolation,
+as that does not work with strict CSP configs. Use `css` and `cx` for
+conditional styling.
+
+Example:
+
+```tsx
+// Correct
+
+/** component.tsx */
+return ;
+```
+
+### No nested classes
+
+We should refrain from using nested `className`. Create the corresponding `css`
+helpers instead and give them sensible names.
+
+Example:
+
+```tsx
+// Wrong
+
+/** component.styles.tsx */
+const wrapper = css`
+ .label {
+ // styles here...
+ }
+
+ .description {
+ // styles here...
+ }
+`;
+
+/** component.tsx */
+return (
+
+
+ Lorem ipsum dolar sit amet...
+
+);
+```
+
+```tsx
+// Correct
+
+/** component.styles.tsx */
+const wrapper = css`
+ // styles here...
+`;
+
+const label = css`
+ // styles here...
+`;
+
+const description = css`
+ // styles here...
+`;
+
+/** component.tsx */
+return (
+
+
+ Lorem ipsum dolar sit amet...
+
+);
+```
+
+### Handling dynamic styles from props
+
+Avoid setting the `style` attribute as it could be blocked with strict CSP
+configs. Set CSS variables through Javascript. Variable names should be
+formatted as `fds---` to ensure uniqueness.
+
+Example:
+
+```tsx
+// Wrong
+
+/** component.tsx */
+const MyComponent = ({ textColour, ...otherProps }) => {
+ return ;
+};
+```
+
+```tsx
+// Correct
+
+/** component.styles.tsx */
+const wrapper = css`
+ /* make sure to reset the variable */
+ --fds-myComponent-wrapper-textColour: initial;
+ color: var(--fds-myComponent-wrapper-textColour);
+`
+
+/** component.tsx */
+const MyComponent = ({
+ textColour,
+ ...otherProps
+}) => {
+ const ref = useRef(null);
+
+ useLayoutEffect(() => {
+ const element = ref.current;
+ if (!element) return;
+
+ element.style.setProperty("--fds-myComponent-wrapper-textColour", ...);
+ }, [textColour]);
+
+ return
+}
+```
+
+## Implementation logics
+
+In cases when you are conditionally rendering based props with multiple variants
+or types, we recommend to use `switch-case` rather than `if-else` to ensure all
+variations are covered.
+
+Example:
+
+```tsx
+interface Demo {
+ variant?: "a" | "b" | "c" | "d" | undefined;
+}
+
+/** In the conditional logic */
+
+// Wrong
+
+if (variant === "a") {
+ // do something
+} else if (variant === "b") {
+ // do something
+} else if (variant === "c") {
+ // do something
+} else if (variant === "d") {
+ // do something
+} else {
+ // do something
+}
+
+// Correct
+switch (variants) {
+ case "a":
+ // do something
+ case "b":
+ // do something
+ case "c":
+ // do something
+ case "d":
+ // do something
+ default:
+ // do something
+}
+```
diff --git a/README.md b/README.md
index f22d60304..a20d944ed 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,18 @@ A React component library for LifeSG and BookingSG related products.
npm i @lifesg/react-design-system
```
+### Peer dependencies
+
+```json
+{
+ "@floating-ui/react": ">=0.26.23 <1.0.0",
+ "@lifesg/react-icons": "^1.5.0",
+ "react": "^17.0.2 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0",
+ "styled-components": "^6.1.19"
+}
+```
+
## Getting Started
diff --git a/codemods/codemod-utils.ts b/codemods/codemod-utils.ts
new file mode 100644
index 000000000..ccd7bdcd3
--- /dev/null
+++ b/codemods/codemod-utils.ts
@@ -0,0 +1,159 @@
+import {
+ API,
+ ASTPath,
+ Collection,
+ JSCodeshift,
+ MemberExpression,
+} from "jscodeshift";
+
+export namespace CodemodUtils {
+ export function hasImport(
+ source: Collection,
+ api: API,
+ importPath: string | string[],
+ importSpecifier: string
+ ) {
+ const j: JSCodeshift = api.jscodeshift;
+
+ let imported = false;
+ source.find(j.ImportDeclaration).forEach((path) => {
+ const currentImportPath = path.node.source.value as string;
+
+ if (Array.isArray(importPath)) {
+ if (!importPath.includes(currentImportPath)) {
+ return;
+ }
+ } else {
+ if (currentImportPath !== importPath) {
+ return;
+ }
+ }
+
+ path.node.specifiers?.forEach((specifier) => {
+ if (
+ j.ImportSpecifier.check(specifier) &&
+ specifier.imported.name === importSpecifier
+ ) {
+ imported = true;
+ }
+ });
+ });
+
+ return imported;
+ }
+
+ export function addImport(
+ source: Collection,
+ api: API,
+ importPath: string,
+ importSpecifier: string
+ ) {
+ const j: JSCodeshift = api.jscodeshift;
+
+ const hasExistingImport =
+ source.find(j.ImportDeclaration, {
+ source: { value: importPath },
+ }).length > 0;
+
+ if (hasExistingImport) {
+ // Append the import
+ source.find(j.ImportDeclaration).forEach((path) => {
+ const currentImportPath = path.node.source.value;
+
+ if (currentImportPath === importPath) {
+ const hasSpecifier =
+ j(path).find(j.ImportSpecifier, {
+ imported: { name: importSpecifier },
+ }).length > 0;
+
+ if (!hasSpecifier) {
+ // Add to existing import
+ path.node.specifiers?.push(
+ j.importSpecifier(j.identifier(importSpecifier))
+ );
+ }
+ }
+ });
+ } else {
+ // Add the import
+ const newImportDeclaration = j.importDeclaration(
+ [j.importSpecifier(j.identifier(importSpecifier))],
+ j.literal(importPath)
+ );
+
+ source.get().node.program.body.unshift(newImportDeclaration);
+ }
+ }
+
+ export function removeImport(
+ source: Collection,
+ api: API,
+ importPath: string | string[],
+ importSpecifier: string
+ ) {
+ const j: JSCodeshift = api.jscodeshift;
+
+ source.find(j.ImportDeclaration).forEach((path) => {
+ const currentImportPath = path.node.source.value as string;
+
+ if (Array.isArray(importPath)) {
+ if (!importPath.includes(currentImportPath)) {
+ return;
+ }
+ } else {
+ if (currentImportPath !== importPath) {
+ return;
+ }
+ }
+
+ const specifiers = j(path).find(j.ImportSpecifier, {
+ imported: { name: importSpecifier },
+ });
+
+ if (specifiers.length) {
+ specifiers.remove();
+
+ // Remove entire import if no longer used
+ const unused = j(path).find(j.ImportSpecifier).length === 0;
+ if (unused) {
+ j(path).remove();
+ }
+ }
+ });
+ }
+
+ export function hasReferences(
+ source: Collection,
+ api: API,
+ importPath: string | string[],
+ importSpecifier: string
+ ) {
+ const j: JSCodeshift = api.jscodeshift;
+
+ const imported = hasImport(source, api, importPath, importSpecifier);
+ if (imported) {
+ return (
+ source
+ .find(j.Identifier, { name: importSpecifier })
+ .filter((id) => {
+ // found in the import statement, does not count as usage
+ return !j(id).closest(j.ImportDeclaration).length;
+ }).length > 0
+ );
+ }
+
+ return false;
+ }
+
+ export function getObjectPath(
+ source: Collection,
+ api: API,
+ path: ASTPath
+ ) {
+ const j: JSCodeshift = api.jscodeshift;
+
+ if (j.MemberExpression.check(path.node)) {
+ return j(path.node).toSource();
+ }
+ }
+}
diff --git a/codemods/common.ts b/codemods/common.ts
new file mode 100644
index 000000000..8853c6a80
--- /dev/null
+++ b/codemods/common.ts
@@ -0,0 +1,8 @@
+export enum Theme {
+ LifeSG = "lifesg",
+ BookingSG = "bookingsg",
+ MyLegacy = "mylegacy",
+ CCube = "ccube",
+ RBS = "rbs",
+ OneService = "oneservice",
+}
diff --git a/codemods/deprecate-v2-tokens/data.ts b/codemods/deprecate-v2-tokens/data.ts
new file mode 100644
index 000000000..c52650f2d
--- /dev/null
+++ b/codemods/deprecate-v2-tokens/data.ts
@@ -0,0 +1,283 @@
+export const componentMap = [
+ {
+ oldName: "DesignToken",
+ newName: "V2_DesignToken",
+ },
+ {
+ oldName: "DesignTokenSet",
+ newName: "V2_DesignTokenSet",
+ },
+ {
+ oldName: "DesignTokenSetOptions",
+ newName: "V2_DesignTokenSetOptions",
+ },
+ {
+ oldName: "MediaQuery",
+ newName: "V2_MediaQuery",
+ },
+ {
+ oldName: "MediaWidths",
+ newName: "V2_MediaWidths",
+ },
+ {
+ oldName: "MediaWidth",
+ newName: "V2_MediaWidth",
+ },
+ {
+ oldName: "MediaType",
+ newName: "V2_MediaType",
+ },
+ {
+ oldName: "Color",
+ newName: "V2_Color",
+ },
+ {
+ oldName: "ColorSet",
+ newName: "V2_ColorSet",
+ },
+ {
+ oldName: "ValidationElementAttributes",
+ newName: "V2_ValidationElementAttributes",
+ },
+ {
+ oldName: "ValidationTypes",
+ newName: "V2_ValidationTypes",
+ },
+ {
+ oldName: "ColorSetOptions",
+ newName: "V2_ColorSetOptions",
+ },
+ {
+ oldName: "Text",
+ newName: "V2_Text",
+ },
+ {
+ oldName: "TextStyleHelper",
+ newName: "V2_TextStyleHelper",
+ },
+ {
+ oldName: "TextStyle",
+ newName: "V2_TextStyle",
+ },
+ {
+ oldName: "TextSizeType",
+ newName: "V2_TextSizeType",
+ },
+ {
+ oldName: "TextLinkSizeType",
+ newName: "V2_TextLinkSizeType",
+ },
+ {
+ oldName: "TextStyleSpec",
+ newName: "V2_TextStyleSpec",
+ },
+ {
+ oldName: "TextStyleSetType",
+ newName: "V2_TextStyleSetType",
+ },
+ {
+ oldName: "TextStyleSetOptionsType",
+ newName: "V2_TextStyleSetOptionsType",
+ },
+ {
+ oldName: "TextWeight",
+ newName: "V2_TextWeight",
+ },
+ {
+ oldName: "TextProps",
+ newName: "V2_TextProps",
+ },
+ {
+ oldName: "TextLinkProps",
+ newName: "V2_TextLinkProps",
+ },
+ {
+ oldName: "TextLinkStyleProps",
+ newName: "V2_TextLinkStyleProps",
+ },
+ {
+ oldName: "Layout",
+ newName: "V2_Layout",
+ },
+ {
+ oldName: "ColDiv",
+ newName: "V2_ColDiv",
+ },
+ {
+ oldName: "Container",
+ newName: "V2_Container",
+ },
+ {
+ oldName: "Content",
+ newName: "V2_Content",
+ },
+ {
+ oldName: "Section",
+ newName: "V2_Section",
+ },
+ {
+ oldName: "CommonLayoutProps",
+ newName: "V2_CommonLayoutProps",
+ },
+ {
+ oldName: "SectionProps",
+ newName: "V2_SectionProps",
+ },
+ {
+ oldName: "ContainerType",
+ newName: "V2_ContainerType",
+ },
+ {
+ oldName: "ContainerProps",
+ newName: "V2_ContainerProps",
+ },
+ {
+ oldName: "ContentProps",
+ newName: "V2_ContentProps",
+ },
+ {
+ oldName: "DivRef",
+ newName: "V2_DivRef",
+ },
+ {
+ oldName: "ColProps",
+ newName: "V2_ColProps",
+ },
+ {
+ oldName: "ColDivProps",
+ newName: "V2_ColDivProps",
+ },
+ {
+ oldName: "TextList",
+ newName: "V2_TextList",
+ },
+ {
+ oldName: "OrderedListProps",
+ newName: "V2_OrderedListProps",
+ },
+ {
+ oldName: "UnorderedListProps",
+ newName: "V2_UnorderedListProps",
+ },
+ {
+ oldName: "CounterType",
+ newName: "V2_CounterType",
+ },
+ {
+ oldName: "BulletType",
+ newName: "V2_BulletType",
+ },
+ {
+ oldName: "Transition",
+ newName: "V2_Transition",
+ },
+ // Added theme name mappings
+ {
+ oldName: "BaseTheme",
+ newName: "V2_BaseTheme",
+ },
+ {
+ oldName: "BookingSGTheme",
+ newName: "V2_BookingSGTheme",
+ },
+ {
+ oldName: "RBSTheme",
+ newName: "V2_RBSTheme",
+ },
+ {
+ oldName: "MyLegacyTheme",
+ newName: "V2_MyLegacyTheme",
+ },
+ {
+ oldName: "CCubeTheme",
+ newName: "V2_CCubeTheme",
+ },
+ {
+ oldName: "OneServiceTheme",
+ newName: "V2_OneServiceTheme",
+ },
+ // Added type name mappings
+ {
+ oldName: "ThemeSpec",
+ newName: "V2_ThemeSpec",
+ },
+ {
+ oldName: "ThemeSpecOptions",
+ newName: "V2_ThemeSpecOptions",
+ },
+ {
+ oldName: "ThemeCollectionSpec",
+ newName: "V2_ThemeCollectionSpec",
+ },
+ {
+ oldName: "ColorScheme",
+ newName: "V2_ColorScheme",
+ },
+ {
+ oldName: "TextStyleScheme",
+ newName: "V2_TextStyleScheme",
+ },
+ {
+ oldName: "DesignTokenScheme",
+ newName: "V2_DesignTokenScheme",
+ },
+ {
+ oldName: "ResourceScheme",
+ newName: "V2_ResourceScheme",
+ },
+ {
+ oldName: "ColorCollectionsMap",
+ newName: "V2_ColorCollectionsMap",
+ },
+ {
+ oldName: "FontStyleCollectionsMap",
+ newName: "V2_FontStyleCollectionsMap",
+ },
+ {
+ oldName: "DesignTokenCollectionsMap",
+ newName: "V2_DesignTokenCollectionsMap",
+ },
+ {
+ oldName: "ThemeContextKeys",
+ newName: "V2_ThemeContextKeys",
+ },
+ {
+ oldName: "ThemeLayout",
+ newName: "V2_ThemeLayout",
+ },
+];
+
+export const pathMap = [
+ {
+ oldPath: "design-token",
+ newPath: "v2_design-token",
+ },
+ {
+ oldPath: "color",
+ newPath: "v2_color",
+ },
+ {
+ oldPath: "media",
+ newPath: "v2_media",
+ },
+ {
+ oldPath: "text",
+ newPath: "v2_text",
+ },
+ {
+ oldPath: "layout",
+ newPath: "v2_layout",
+ },
+ {
+ oldPath: "text-list",
+ newPath: "v2_text-list",
+ },
+ {
+ oldPath: "theme",
+ newPath: "v2_theme",
+ },
+ {
+ oldPath: "transition",
+ newPath: "v2_transition",
+ },
+];
diff --git a/codemods/deprecate-v2-tokens/index.ts b/codemods/deprecate-v2-tokens/index.ts
new file mode 100644
index 000000000..fa380dab2
--- /dev/null
+++ b/codemods/deprecate-v2-tokens/index.ts
@@ -0,0 +1,88 @@
+import { API, FileInfo, JSCodeshift } from "jscodeshift";
+import { componentMap, pathMap } from "./data";
+
+export default function transformer(file: FileInfo, api: API) {
+ const j: JSCodeshift = api.jscodeshift;
+ const source = j(file.source);
+
+ const componentsToChange = new Set();
+
+ // change import declarations and check which components to change
+ source.find(j.ImportDeclaration).forEach((path) => {
+ const importPath = path.node.source.value;
+
+ // check if importPath starts with @lifesg/
+ const isPathLib =
+ typeof importPath === "string" && importPath.startsWith("@lifesg/");
+
+ if (isPathLib) {
+ componentMap.forEach(({ oldName, newName }) => {
+ let importChanged = false;
+
+ // change specifiers
+ if (path.node.specifiers) {
+ path.node.specifiers.forEach((specifier) => {
+ if (
+ specifier.type === "ImportSpecifier" &&
+ specifier.imported.name === oldName
+ ) {
+ specifier.imported.name = newName;
+ if (
+ specifier.local &&
+ specifier.local.name === oldName
+ ) {
+ specifier.local.name = newName;
+ }
+ importChanged = true;
+ componentsToChange.add(oldName);
+ }
+ });
+ }
+
+ // check and update path by replacing the last word with the new path
+ if (
+ importChanged &&
+ importPath !== "@lifesg/react-design-system" &&
+ typeof importPath === "string"
+ ) {
+ const pathParts = importPath.split("/");
+ if (
+ pathParts[pathParts.length - 1] !==
+ "react-design-system"
+ ) {
+ const lastPathPart = pathParts[pathParts.length - 1];
+ // map with pathMap to find the new path
+ pathMap.forEach(({ oldPath, newPath }) => {
+ if (lastPathPart === oldPath) {
+ pathParts[pathParts.length - 1] = newPath;
+ path.node.source.value = pathParts.join("/");
+ }
+ });
+ }
+ }
+ });
+ }
+ });
+
+ // change JSX identifiers and normal identifiers
+ if (componentsToChange.size > 0) {
+ componentMap.forEach(({ oldName, newName }) => {
+ if (componentsToChange.has(oldName)) {
+ source
+ .find(j.JSXIdentifier, { name: oldName })
+ .forEach((path) => {
+ path.node.name = newName;
+ });
+
+ source.find(j.Identifier, { name: oldName }).forEach((path) => {
+ // only update if it's a parent property
+ if (!j.MemberExpression.check(path.parent.value.object)) {
+ path.node.name = newName;
+ }
+ });
+ }
+ });
+ }
+
+ return source.toSource();
+}
diff --git a/codemods/migrate-colour/data.ts b/codemods/migrate-colour/data.ts
new file mode 100644
index 000000000..d13d8b582
--- /dev/null
+++ b/codemods/migrate-colour/data.ts
@@ -0,0 +1,95 @@
+const defaultMapping = {
+ Primary: "primary-50",
+ PrimaryDark: "primary-40",
+ Secondary: "primary-40",
+ "Brand[1]": "brand-50",
+ "Brand[2]": "brand-60",
+ "Brand[3]": "brand-70",
+ "Brand[4]": "brand-80",
+ "Brand[5]": "brand-90",
+ "Brand[6]": "brand-95",
+ "Accent.Light[1]": "primary-60",
+ "Accent.Light[2]": "primary-70",
+ "Accent.Light[3]": "primary-80",
+ "Accent.Light[4]": "primary-90",
+ "Accent.Light[5]": "primary-95",
+ "Accent.Light[6]": "primary-100",
+ "Accent.Dark[1]": "secondary-40",
+ "Accent.Dark[2]": "secondary-50",
+ "Accent.Dark[3]": "secondary-60",
+ "Neutral[1]": "neutral-20",
+ "Neutral[2]": "neutral-30",
+ "Neutral[3]": "neutral-50",
+ "Neutral[4]": "neutral-60",
+ "Neutral[5]": "neutral-90",
+ "Neutral[6]": "neutral-95",
+ "Neutral[7]": "neutral-100",
+ "Neutral[8]": "white",
+ "Validation.Green.Text": "success-40",
+ "Validation.Green.Icon": "success-70",
+ "Validation.Green.Border": "success-80",
+ "Validation.Green.Background": "success-100",
+ "Validation.Orange.Text": "warning-50",
+ "Validation.Orange.Icon": "warning-70",
+ "Validation.Orange.Border": "warning-80",
+ "Validation.Orange.Background": "warning-100",
+ "Validation.Orange.Badge": "warning-100",
+ "Validation.Red.Text": "error-50",
+ "Validation.Red.Icon": "error-50",
+ "Validation.Red.Border": "error-60",
+ "Validation.Red.Background": "error-100",
+ "Validation.Blue.Text": "info-40",
+ "Validation.Blue.Icon": "info-50",
+ "Validation.Blue.Border": "info-70",
+ "Validation.Blue.Background": "info-95",
+};
+
+export const lifesgMapping = {
+ ...defaultMapping,
+ Primary: "primary-50",
+ PrimaryDark: "primary-40",
+ Secondary: "primary-40",
+};
+
+export const bookingSgMapping = {
+ ...defaultMapping,
+ Primary: "primary-50",
+ PrimaryDark: "primary-40",
+ Secondary: "primary-40",
+ "Accent.Dark[1]": "secondary-40",
+ "Accent.Dark[2]": "secondary-60",
+ "Accent.Dark[3]": "secondary-70",
+};
+
+export const mylegacyMapping = {
+ ...defaultMapping,
+ PrimaryDark: "primary-40",
+ Primary: "primary-50",
+ Secondary: "primary-40",
+ "Validation.Green.Text": "success-50",
+ "Validation.Red.Text": "success-40",
+};
+
+export const ccubeMapping = {
+ ...defaultMapping,
+ Primary: "primary-50",
+ PrimaryDark: "primary-40",
+ Secondary: "secondary-40",
+ "Brand[1]": "brand-60",
+ "Brand[2]": "brand-50",
+};
+
+export const rbsMapping = {
+ ...defaultMapping,
+};
+
+export const oneServiceMapping = {
+ ...defaultMapping,
+ Primary: "primary-50",
+ PrimaryDark: "primary-40",
+ Secondary: "secondary-40",
+ "Brand[1]": "brand-60",
+ "Accent.Dark[1]": "secondary-50",
+ "Accent.Dark[2]": "secondary-60",
+ "Accent.Dark[3]": "secondary-70",
+};
diff --git a/codemods/migrate-colour/index.ts b/codemods/migrate-colour/index.ts
new file mode 100644
index 000000000..83848c9bd
--- /dev/null
+++ b/codemods/migrate-colour/index.ts
@@ -0,0 +1,103 @@
+import { API, FileInfo, JSCodeshift } from "jscodeshift";
+import { CodemodUtils } from "../codemod-utils";
+import { Theme } from "../common";
+import {
+ bookingSgMapping,
+ ccubeMapping,
+ lifesgMapping,
+ mylegacyMapping,
+ oneServiceMapping,
+ rbsMapping,
+} from "./data";
+
+const IMPORT_PATHS = {
+ V2_COLOR: "@lifesg/react-design-system/v2_color",
+ DESIGN_SYSTEM: "@lifesg/react-design-system",
+ THEME: "@lifesg/react-design-system/theme",
+};
+
+const IMPORT_SPECIFIERS = {
+ V2_COLOR: "V2_Color",
+ COLOUR: "Colour",
+};
+
+const MEMBER_EXPRESSION_PROPERTIES = {
+ PRIMITIVE: "Primitive",
+};
+
+const COLOR_MAPPINGS = {
+ lifesg: lifesgMapping,
+ bookingsg: bookingSgMapping,
+ mylegacy: mylegacyMapping,
+ ccube: ccubeMapping,
+ rbs: rbsMapping,
+ oneservice: oneServiceMapping,
+};
+
+interface Options {
+ mapping: Theme;
+}
+
+export default function transformer(
+ file: FileInfo,
+ api: API,
+ options: Options
+) {
+ const j: JSCodeshift = api.jscodeshift;
+ const source = j(file.source);
+
+ const isV2ColorImport = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_COLOR],
+ IMPORT_SPECIFIERS.V2_COLOR
+ );
+
+ // Determine which Colour mapping to use
+ const colorMapping =
+ COLOR_MAPPINGS[options.mapping] || COLOR_MAPPINGS[Theme.LifeSG];
+
+ const replaceWithColorPrimitive = (path: any, new_color_value: string) => {
+ path.replace(
+ j.memberExpression(
+ j.memberExpression(
+ j.identifier(IMPORT_SPECIFIERS.COLOUR),
+ j.identifier(MEMBER_EXPRESSION_PROPERTIES.PRIMITIVE)
+ ),
+ j.literal(new_color_value)
+ )
+ );
+ };
+
+ if (isV2ColorImport) {
+ CodemodUtils.addImport(
+ source,
+ api,
+ IMPORT_PATHS.THEME,
+ IMPORT_SPECIFIERS.COLOUR
+ );
+
+ CodemodUtils.removeImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_COLOR],
+ IMPORT_SPECIFIERS.V2_COLOR
+ );
+
+ // Map V2 Color usage to V3 Colour
+ source.find(j.MemberExpression).forEach((path) => {
+ const objectPath = CodemodUtils.getObjectPath(source, api, path);
+ const prefix = IMPORT_SPECIFIERS.V2_COLOR + ".";
+ if (objectPath && objectPath.startsWith(prefix)) {
+ const colour = objectPath.slice(prefix.length);
+ const newColorValue =
+ colorMapping[colour as keyof typeof colorMapping];
+ if (newColorValue) {
+ replaceWithColorPrimitive(path, newColorValue);
+ }
+ }
+ });
+ }
+
+ return source.toSource();
+}
diff --git a/codemods/migrate-layout/data.ts b/codemods/migrate-layout/data.ts
new file mode 100644
index 000000000..4b4274423
--- /dev/null
+++ b/codemods/migrate-layout/data.ts
@@ -0,0 +1,5 @@
+export const propMapping = {
+ desktopCols: "xlCols",
+ tabletCols: "mdCols",
+ mobileCols: "xxsCols",
+};
diff --git a/codemods/migrate-layout/index.ts b/codemods/migrate-layout/index.ts
new file mode 100644
index 000000000..253a488ad
--- /dev/null
+++ b/codemods/migrate-layout/index.ts
@@ -0,0 +1,107 @@
+import { API, FileInfo, JSCodeshift } from "jscodeshift";
+import { CodemodUtils } from "../codemod-utils";
+import { propMapping } from "./data";
+
+const IMPORT_PATHS = {
+ V2_LAYOUT: "@lifesg/react-design-system/v2_layout",
+ DESIGN_SYSTEM: "@lifesg/react-design-system",
+ LAYOUT: "@lifesg/react-design-system/layout",
+};
+
+const IMPORT_SPECIFIERS = {
+ V2_LAYOUT: "V2_Layout",
+ LAYOUT: "Layout",
+};
+
+export default function transformer(file: FileInfo, api: API) {
+ const j: JSCodeshift = api.jscodeshift;
+ const source = j(file.source);
+
+ const isLifesgImport = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_LAYOUT],
+ IMPORT_SPECIFIERS.V2_LAYOUT
+ );
+
+ if (isLifesgImport) {
+ CodemodUtils.addImport(
+ source,
+ api,
+ IMPORT_PATHS.LAYOUT,
+ IMPORT_SPECIFIERS.LAYOUT
+ );
+
+ CodemodUtils.removeImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_LAYOUT],
+ IMPORT_SPECIFIERS.V2_LAYOUT
+ );
+
+ // Update V2_Layout to Layout
+ source.find(j.JSXMemberExpression).forEach((path) => {
+ const { object } = path.node;
+
+ if (
+ j.JSXIdentifier.check(object) &&
+ object.name === IMPORT_SPECIFIERS.V2_LAYOUT
+ ) {
+ object.name = IMPORT_SPECIFIERS.LAYOUT;
+ }
+ });
+
+ source
+ .find(j.Identifier, { name: IMPORT_SPECIFIERS.V2_LAYOUT })
+ .forEach((path) => {
+ path.node.name = IMPORT_SPECIFIERS.LAYOUT;
+ });
+
+ // Update ColDiv column props to its V3 version
+ source.find(j.JSXOpeningElement).forEach((path) => {
+ const { name, attributes } = path.node;
+
+ if (
+ j.JSXMemberExpression.check(name) &&
+ j.JSXIdentifier.check(name.object) &&
+ name.object.name === IMPORT_SPECIFIERS.LAYOUT &&
+ j.JSXIdentifier.check(name.property) &&
+ name.property.name === "ColDiv"
+ ) {
+ if (attributes && attributes.length > 0) {
+ attributes.forEach((attribute) => {
+ if (
+ j.JSXAttribute.check(attribute) &&
+ j.JSXIdentifier.check(attribute.name)
+ ) {
+ const oldPropName = attribute.name.name;
+ const newPropName =
+ propMapping[
+ oldPropName as keyof typeof propMapping
+ ];
+
+ if (newPropName) {
+ attribute.name.name = newPropName;
+ }
+ }
+ });
+ }
+ }
+ });
+
+ source.find(j.JSXElement).forEach((path) => {
+ const openingElement = path.node.openingElement;
+ const { name } = openingElement;
+ if (
+ j.JSXMemberExpression.check(name) &&
+ j.JSXIdentifier.check(name.object)
+ ) {
+ if (name.object.name === IMPORT_SPECIFIERS.V2_LAYOUT) {
+ name.object.name = IMPORT_SPECIFIERS.LAYOUT;
+ }
+ }
+ });
+ }
+
+ return source.toSource();
+}
diff --git a/codemods/migrate-media-query/data.ts b/codemods/migrate-media-query/data.ts
new file mode 100644
index 000000000..a923eeec6
--- /dev/null
+++ b/codemods/migrate-media-query/data.ts
@@ -0,0 +1,20 @@
+export const mediaQueryMap = {
+ MaxWidth: {
+ mobileS: "xxs",
+ mobileM: "xs",
+ mobileL: "sm",
+ tablet: "lg",
+ desktopM: "xl",
+ desktopL: "xl",
+ desktop4k: "xl",
+ },
+ MinWidth: {
+ mobileS: "xs",
+ mobileM: "sm",
+ mobileL: "md",
+ tablet: "xl",
+ desktopM: "xxl",
+ desktopL: "xxl",
+ desktop4k: "xxl",
+ },
+};
diff --git a/codemods/migrate-media-query/index.ts b/codemods/migrate-media-query/index.ts
new file mode 100644
index 000000000..2e09d21e6
--- /dev/null
+++ b/codemods/migrate-media-query/index.ts
@@ -0,0 +1,112 @@
+import { API, FileInfo, JSCodeshift } from "jscodeshift";
+import { CodemodUtils } from "../codemod-utils";
+import { mediaQueryMap } from "./data";
+
+// ======= Constants ======= //
+
+const IMPORT_PATHS = {
+ V2_MEDIA: "@lifesg/react-design-system/v2_media",
+ DESIGN_SYSTEM: "@lifesg/react-design-system",
+ THEME: "@lifesg/react-design-system/theme",
+};
+
+const IMPORT_SPECIFIERS = {
+ V2_MEDIA_QUERY: "V2_MediaQuery",
+ MEDIA_QUERY: "MediaQuery",
+ V2_MEDIA_WIDTHS: "V2_MediaWidths",
+};
+
+const MEMBER_EXPRESSION_PROPERTIES = {
+ MAX_WIDTH: "MaxWidth",
+ MIN_WIDTH: "MinWidth",
+};
+
+const WARNINGS = {
+ DEPRECATED_USAGE: `\x1b[31m[MIGRATION] V2_MediaWidths requires manual migration to Breakpoint. Refer to <"https://github.com/LifeSG/react-design-system/wiki/Migrating-to-V3">. File:\x1b[0m`,
+};
+
+const IDENTIFIERS = {
+ MEDIA_QUERY: "MediaQuery",
+};
+
+// ======= Transformer Function ======= //
+export default function transformer(file: FileInfo, api: API) {
+ const j: JSCodeshift = api.jscodeshift;
+ const source = j(file.source);
+
+ const isV2MediaQueryImported = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_MEDIA],
+ IMPORT_SPECIFIERS.V2_MEDIA_QUERY
+ );
+ const isV2MediaWidthsImported = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_MEDIA],
+ IMPORT_SPECIFIERS.V2_MEDIA_WIDTHS
+ );
+
+ // Detect usage of V2_MediaQuery
+ // Replace V2_MediaQuery with V3 MediaQuery
+ if (isV2MediaQueryImported) {
+ CodemodUtils.addImport(
+ source,
+ api,
+ IMPORT_PATHS.THEME,
+ IMPORT_SPECIFIERS.MEDIA_QUERY
+ );
+
+ CodemodUtils.removeImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_MEDIA],
+ IMPORT_SPECIFIERS.V2_MEDIA_QUERY
+ );
+
+ // Rename all instances of V2_MediaQuery to MediaQuery
+ source
+ .find(j.Identifier, { name: IMPORT_SPECIFIERS.V2_MEDIA_QUERY })
+ .forEach((path) => {
+ path.node.name = IMPORT_SPECIFIERS.MEDIA_QUERY;
+ });
+
+ // Map V2 to V3 breakpoints
+ source.find(j.MemberExpression).forEach((path) => {
+ const object = path.node.object;
+ const property = path.node.property;
+
+ if (
+ j.MemberExpression.check(object) &&
+ j.Identifier.check(object.object) &&
+ object.object.name === IDENTIFIERS.MEDIA_QUERY &&
+ j.Identifier.check(object.property) &&
+ (object.property.name ===
+ MEMBER_EXPRESSION_PROPERTIES.MAX_WIDTH ||
+ object.property.name ===
+ MEMBER_EXPRESSION_PROPERTIES.MIN_WIDTH) &&
+ j.Identifier.check(property)
+ ) {
+ const queryType = object.property
+ .name as keyof typeof mediaQueryMap;
+ const mediaKey =
+ property.name as keyof (typeof mediaQueryMap)[typeof queryType];
+
+ if (
+ mediaQueryMap[queryType] &&
+ mediaQueryMap[queryType][mediaKey]
+ ) {
+ const newMediaKey = mediaQueryMap[queryType][mediaKey];
+ property.name = newMediaKey;
+ }
+ }
+ });
+ }
+
+ // Link to migration docs for V2_MediaWidths deprecation
+ if (isV2MediaWidthsImported) {
+ console.error(`${WARNINGS.DEPRECATED_USAGE} ${file.path}`);
+ }
+
+ return source.toSource();
+}
diff --git a/codemods/migrate-text-list/data.ts b/codemods/migrate-text-list/data.ts
new file mode 100644
index 000000000..433fbd6b0
--- /dev/null
+++ b/codemods/migrate-text-list/data.ts
@@ -0,0 +1,16 @@
+export const sizePropMapping = {
+ D1: "heading-xxl",
+ D2: "heading-xl",
+ D3: "heading-md",
+ D4: "heading-sm",
+ H1: "heading-lg",
+ H2: "heading-md",
+ H3: "heading-sm",
+ H4: "heading-xs",
+ H5: "body-md",
+ H6: "body-sm",
+ DBody: "heading-sm",
+ Body: "body-baseline",
+ BodySmall: "body-md",
+ XSmall: "body-xs",
+};
diff --git a/codemods/migrate-text-list/index.ts b/codemods/migrate-text-list/index.ts
new file mode 100644
index 000000000..f5080c297
--- /dev/null
+++ b/codemods/migrate-text-list/index.ts
@@ -0,0 +1,110 @@
+import { API, FileInfo, JSCodeshift } from "jscodeshift";
+import { CodemodUtils } from "../codemod-utils";
+import { sizePropMapping } from "./data";
+
+// ======= Constants ======= //
+
+const IMPORT_PATHS = {
+ TEXT_LIST: "@lifesg/react-design-system/text-list",
+ V2_TEXT_LIST: "@lifesg/react-design-system/v2_text-list",
+ DESIGN_SYSTEM: "@lifesg/react-design-system",
+};
+
+const IMPORT_SPECIFIERS = {
+ TEXT_LIST: "TextList",
+ V2_TEXT_LIST: "V2_TextList",
+};
+
+const JSX_IDENTIFIERS = {
+ TEXT_LIST: "TextList",
+ V2_TEXT_LIST: "V2_TextList",
+};
+
+// ======= Transformer Function ======= //
+
+export default function transformer(file: FileInfo, api: API) {
+ const j: JSCodeshift = api.jscodeshift;
+ const source = j(file.source);
+
+ const isV2TextListImport = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT_LIST],
+ IMPORT_SPECIFIERS.V2_TEXT_LIST
+ );
+
+ if (isV2TextListImport) {
+ CodemodUtils.addImport(
+ source,
+ api,
+ IMPORT_PATHS.TEXT_LIST,
+ IMPORT_SPECIFIERS.TEXT_LIST
+ );
+
+ CodemodUtils.removeImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT_LIST],
+ IMPORT_SPECIFIERS.V2_TEXT_LIST
+ );
+
+ source
+ .find(j.Identifier, { name: JSX_IDENTIFIERS.V2_TEXT_LIST })
+ .forEach((path) => {
+ path.node.name = JSX_IDENTIFIERS.TEXT_LIST;
+ });
+
+ source.find(j.MemberExpression).forEach((path) => {
+ if (
+ j.Identifier.check(path.node.object) &&
+ path.node.object.name === JSX_IDENTIFIERS.V2_TEXT_LIST
+ ) {
+ path.node.object.name = JSX_IDENTIFIERS.TEXT_LIST;
+ }
+ });
+
+ source.find(j.JSXMemberExpression).forEach((path) => {
+ if (
+ j.JSXIdentifier.check(path.node.object) &&
+ path.node.object.name === JSX_IDENTIFIERS.V2_TEXT_LIST
+ ) {
+ path.node.object.name = JSX_IDENTIFIERS.TEXT_LIST;
+ }
+ });
+
+ source.find(j.JSXOpeningElement).forEach((path) => {
+ const openingElement = path.node;
+ let isTextListElement = false;
+
+ if (
+ j.JSXMemberExpression.check(openingElement.name) &&
+ j.JSXIdentifier.check(openingElement.name.object) &&
+ openingElement.name.object.name === JSX_IDENTIFIERS.TEXT_LIST
+ ) {
+ isTextListElement = true;
+ }
+
+ if (isTextListElement) {
+ const attributes = openingElement.attributes;
+
+ attributes?.forEach((attribute) => {
+ if (
+ j.JSXAttribute.check(attribute) &&
+ attribute.name.name === "size" &&
+ j.StringLiteral.check(attribute.value)
+ ) {
+ const sizeValue = attribute.value
+ .value as keyof typeof sizePropMapping;
+ if (sizePropMapping[sizeValue]) {
+ attribute.value = j.literal(
+ sizePropMapping[sizeValue]
+ );
+ }
+ }
+ });
+ }
+ });
+ }
+
+ return source.toSource();
+}
diff --git a/codemods/migrate-text/data.ts b/codemods/migrate-text/data.ts
new file mode 100644
index 000000000..e813b097c
--- /dev/null
+++ b/codemods/migrate-text/data.ts
@@ -0,0 +1,48 @@
+export const textComponentMap = {
+ D1: "HeadingXXL",
+ D2: "HeadingXL",
+ D3: "HeadingMD",
+ D4: "HeadingSM",
+ H1: "HeadingLG",
+ H2: "HeadingMD",
+ H3: "HeadingSM",
+ H4: "HeadingXS",
+ H5: "BodyMD",
+ H6: "BodySM",
+ DBody: "HeadingSM",
+ Body: "BodyBL",
+ BodySmall: "BodyMD",
+ XSmall: "BodyXS",
+ "Hyperlink.Default": "LinkBL",
+ "Hyperlink.Small": "LinkMD",
+};
+
+export const textStyleFontMap = {
+ D1: "heading-xxl",
+ D2: "heading-xl",
+ D3: "heading-md",
+ D4: "heading-sm",
+ H1: "heading-lg",
+ H2: "heading-md",
+ H3: "heading-sm",
+ H4: "heading-xs",
+ H5: "body-md",
+ H6: "body-sm",
+ DBody: "heading-sm",
+ Body: "body-baseline",
+ BodySmall: "body-md",
+ XSmall: "body-xs",
+};
+
+export const weightMap = {
+ regular: "regular",
+ semibold: "semibold",
+ bold: "bold",
+ light: "light",
+ black: "bold",
+ 400: "regular",
+ 600: "semibold",
+ 700: "bold",
+ 300: "light",
+ 900: "bold",
+};
diff --git a/codemods/migrate-text/index.ts b/codemods/migrate-text/index.ts
new file mode 100644
index 000000000..6e9f6367c
--- /dev/null
+++ b/codemods/migrate-text/index.ts
@@ -0,0 +1,195 @@
+import {
+ API,
+ ASTPath,
+ FileInfo,
+ JSCodeshift,
+ MemberExpression,
+} from "jscodeshift";
+import { CodemodUtils } from "../codemod-utils";
+import { textComponentMap, textStyleFontMap, weightMap } from "./data";
+
+// ======= Constants ======= //
+
+const IMPORT_PATHS = {
+ V2_TEXT: "@lifesg/react-design-system/v2_text",
+ DESIGN_SYSTEM: "@lifesg/react-design-system",
+ TYPOGRAPHY: "@lifesg/react-design-system/typography",
+ THEME: "@lifesg/react-design-system/theme",
+};
+
+const IMPORT_SPECIFIERS = {
+ V2_TEXT: "V2_Text",
+ V2_TEXT_STYLE_HELPER: "V2_TextStyleHelper",
+ TYPOGRAPHY: "Typography",
+ FONT: "Font",
+};
+
+const JSX_IDENTIFIERS = {
+ V2_TEXT: "V2_Text",
+ TYPOGRAPHY: "Typography",
+};
+
+const IDENTIFIERS = {
+ V2_GET_TEXT_STYLE: "getTextStyle",
+};
+
+// ======= Transformer Function ======= //
+
+export default function transformer(file: FileInfo, api: API) {
+ const j: JSCodeshift = api.jscodeshift;
+ const source = j(file.source);
+
+ const importsText = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT],
+ IMPORT_SPECIFIERS.V2_TEXT
+ );
+ const importsTextStyleHelper = CodemodUtils.hasImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT],
+ IMPORT_SPECIFIERS.V2_TEXT_STYLE_HELPER
+ );
+
+ const replaceWithTypography = (
+ path: ASTPath,
+ newComponentValue: string
+ ) => {
+ path.replace(
+ j.memberExpression(
+ j.identifier(JSX_IDENTIFIERS.TYPOGRAPHY),
+ j.identifier(newComponentValue)
+ )
+ );
+ };
+
+ if (importsText) {
+ CodemodUtils.addImport(
+ source,
+ api,
+ IMPORT_PATHS.TYPOGRAPHY,
+ IMPORT_SPECIFIERS.TYPOGRAPHY
+ );
+
+ CodemodUtils.removeImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT],
+ IMPORT_SPECIFIERS.V2_TEXT
+ );
+
+ source
+ .find(j.Identifier, { name: JSX_IDENTIFIERS.V2_TEXT })
+ .forEach((path) => {
+ // Replace V2_Text with Typography
+ path.node.name = JSX_IDENTIFIERS.TYPOGRAPHY;
+ });
+
+ // Map to respective V3 Typography component usage
+ source.find(j.MemberExpression).forEach((path) => {
+ const propertyNameParts: string[] = [];
+ let currentPath = path.node;
+ let startsWithTypography = false;
+
+ while (j.MemberExpression.check(currentPath)) {
+ const property = currentPath.property;
+ const object = currentPath.object;
+
+ if (j.Identifier.check(property)) {
+ propertyNameParts.unshift(property.name);
+ }
+
+ if (j.MemberExpression.check(object)) {
+ currentPath = object;
+ } else if (j.Identifier.check(object)) {
+ if (object.name === JSX_IDENTIFIERS.TYPOGRAPHY) {
+ startsWithTypography = true;
+ }
+ break;
+ } else {
+ break;
+ }
+ }
+
+ if (startsWithTypography) {
+ const propertyName = propertyNameParts.join(
+ "."
+ ) as keyof typeof textComponentMap;
+ const newTypographyValue = textComponentMap[propertyName];
+ if (newTypographyValue) {
+ replaceWithTypography(path, newTypographyValue);
+ }
+ }
+ });
+ }
+
+ if (importsTextStyleHelper) {
+ let usesTextStyleHelper = false;
+
+ source.find(j.CallExpression).forEach((path) => {
+ if (
+ !j.CallExpression.check(path.node) ||
+ !j.MemberExpression.check(path.node.callee) ||
+ !j.Identifier.check(path.node.callee.object) ||
+ path.node.callee.object.name !==
+ IMPORT_SPECIFIERS.V2_TEXT_STYLE_HELPER ||
+ !j.Identifier.check(path.node.callee.property) ||
+ path.node.callee.property.name !== IDENTIFIERS.V2_GET_TEXT_STYLE
+ ) {
+ return;
+ }
+
+ const style = j.Literal.check(path.node.arguments[0])
+ ? path.node.arguments[0].value
+ : undefined;
+ const weight = j.Literal.check(path.node.arguments[1])
+ ? path.node.arguments[1].value
+ : "regular";
+
+ if (!style) {
+ return;
+ }
+
+ const mappedType =
+ textStyleFontMap[style as keyof typeof textStyleFontMap];
+ const mappedWeight = weightMap[weight as keyof typeof weightMap];
+
+ j(path).replaceWith(() => {
+ return j.memberExpression(
+ j.identifier("Font"),
+ j.literal(`${mappedType}-${mappedWeight}`)
+ );
+ });
+
+ usesTextStyleHelper = true;
+ });
+
+ if (usesTextStyleHelper) {
+ CodemodUtils.addImport(
+ source,
+ api,
+ IMPORT_PATHS.THEME,
+ IMPORT_SPECIFIERS.FONT
+ );
+ }
+
+ if (
+ !CodemodUtils.hasReferences(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT],
+ IMPORT_SPECIFIERS.V2_TEXT_STYLE_HELPER
+ )
+ ) {
+ CodemodUtils.removeImport(
+ source,
+ api,
+ [IMPORT_PATHS.DESIGN_SYSTEM, IMPORT_PATHS.V2_TEXT],
+ IMPORT_SPECIFIERS.V2_TEXT_STYLE_HELPER
+ );
+ }
+ }
+
+ return source.toSource();
+}
diff --git a/codemods/run-codemod.ts b/codemods/run-codemod.ts
new file mode 100644
index 000000000..8600c6043
--- /dev/null
+++ b/codemods/run-codemod.ts
@@ -0,0 +1,222 @@
+import { checkbox, confirm, input, select } from "@inquirer/prompts";
+import { execSync } from "child_process";
+import * as fs from "fs";
+import * as path from "path";
+import { Theme } from "./common";
+
+const codemodsDir =
+ process.env.ENV === "dev"
+ ? path.join(process.cwd(), "codemods")
+ : path.join(
+ process.cwd(),
+ "node_modules/@lifesg/react-design-system/codemods"
+ );
+
+enum Codemod {
+ DeprecateV2Tokens = "deprecate-v2-tokens",
+ MigrateColour = "migrate-colour",
+ MigrateLayout = "migrate-layout",
+ MigrateMediaQuery = "migrate-media-query",
+ MigrateText = "migrate-text",
+ MigrateTextList = "migrate-text-list",
+}
+
+const theme = {
+ helpMode: "always" as const,
+};
+
+const CodemodDescriptions: { [key in Codemod]: string } = {
+ [Codemod.DeprecateV2Tokens]: "Migrate deprecated V2 imports",
+ [Codemod.MigrateColour]: "Replace V2_Color with new Colour tokens",
+ [Codemod.MigrateLayout]: "Replace V2_Layout with new Layout components",
+ [Codemod.MigrateMediaQuery]:
+ "Replace V2 media queries with new Breakpoint tokens",
+ [Codemod.MigrateText]:
+ "Replace V2_Text with new Typography components and V2_TextStyleHelper.getTextStyle() with Font",
+ [Codemod.MigrateTextList]:
+ "Replace V2_TextList with new Textlist components",
+};
+
+const TargetDirectoryPaths = {
+ src: "src",
+ codebase: ".",
+};
+
+interface UserSelection {
+ selectedCodemods: string[];
+ selectedTheme: Theme | null;
+ targetPath: string;
+}
+
+// Return codemods in codemod folder
+function listCodemods(): { name: string; value: string }[] {
+ const codemods: string[] = fs.readdirSync(codemodsDir).filter((folder) => {
+ return fs.lstatSync(path.join(codemodsDir, folder)).isDirectory();
+ });
+
+ const options = codemods.map((mod) => ({
+ name: mod,
+ value: mod,
+ description: CodemodDescriptions[mod as Codemod],
+ }));
+
+ return options;
+}
+
+function runCodemods(selection: UserSelection): void {
+ const { selectedCodemods, targetPath, selectedTheme } = selection;
+
+ selectedCodemods.forEach((codemod) => {
+ const codemodPath = path.join(codemodsDir, codemod, "index.ts");
+ let command = `npx --yes jscodeshift --parser=tsx -t ${codemodPath} ${targetPath}`;
+
+ if (codemod === Codemod.MigrateColour && selectedTheme) {
+ command = `npx --yes jscodeshift --parser=tsx -t ${codemodPath} --mapping=${selectedTheme} ${targetPath}`;
+ }
+
+ console.log(
+ `Running codemod: ${codemod} on target path: ${targetPath}`
+ );
+
+ try {
+ execSync(command, { stdio: "inherit" });
+ console.log(
+ `Codemod ${codemod} executed successfully on ${targetPath}`
+ );
+ } catch (error) {
+ if (error instanceof Error) {
+ console.error(
+ `Error executing codemod ${codemod}: ${error.message}`
+ );
+ }
+
+ throw error;
+ }
+ });
+}
+
+async function chooseTargetPath(): Promise {
+ const options = [
+ {
+ name: "src (./src)",
+ value: TargetDirectoryPaths.src,
+ },
+ {
+ name: "current directory (./)",
+ value: TargetDirectoryPaths.codebase,
+ },
+ { name: "custom", value: "custom" },
+ ];
+
+ const selectedOption = await select({
+ message: "Choose a target path:",
+ choices: options,
+ });
+
+ if (selectedOption === "custom") {
+ const customPath = await input({
+ message: "Enter your custom path:",
+ required: true,
+ });
+
+ const resolvedPath = path.resolve(customPath);
+
+ if (!fs.existsSync(resolvedPath)) {
+ console.error(`The path "${resolvedPath}" does not exist`);
+ return null;
+ }
+
+ return resolvedPath;
+ }
+
+ return selectedOption;
+}
+
+async function chooseTheme(): Promise {
+ const themeOptions = [
+ { name: "LifeSG", value: Theme.LifeSG },
+ { name: "BookingSG", value: Theme.BookingSG },
+ { name: "MyLegacy", value: Theme.MyLegacy },
+ { name: "CCube", value: Theme.CCube },
+ { name: "RBS", value: Theme.RBS },
+ { name: "OneService", value: Theme.OneService },
+ ];
+
+ const selectedTheme = await select({
+ message: "Select the theme that your project is using:",
+ choices: themeOptions,
+ });
+
+ return selectedTheme;
+}
+
+async function getCodemodSelections(): Promise {
+ const codemods = listCodemods();
+ if (codemods.length === 0) {
+ throw new Error("No codemod scripts found");
+ }
+
+ const selectedCodemods = await checkbox({
+ required: true,
+ message: "Select codemods to run:",
+ choices: codemods,
+ theme,
+ });
+
+ let selectedTheme: Theme | null = null;
+ if (selectedCodemods.includes(Codemod.MigrateColour)) {
+ selectedTheme = await chooseTheme();
+ }
+
+ const targetPath = await chooseTargetPath();
+ if (!targetPath) {
+ throw new Error("No target path selected or provided");
+ }
+
+ return { selectedCodemods, selectedTheme, targetPath };
+}
+
+async function getConfirmation(selection: UserSelection) {
+ const { selectedCodemods, selectedTheme, targetPath } = selection;
+
+ const codemods = selectedCodemods.join(", ");
+ const finalConfirmationMessage =
+ `\nYou are about to run the following codemods: ${codemods}\n` +
+ `Target path: ${targetPath}\n` +
+ `${
+ selectedTheme
+ ? `Selected theme for "migrate-colour": ${selectedTheme}\n`
+ : ""
+ }`;
+
+ console.log(finalConfirmationMessage);
+
+ const finalConfirmation = await confirm({
+ message: "Do you want to proceed?",
+ default: true,
+ });
+
+ return finalConfirmation;
+}
+
+async function main(): Promise {
+ try {
+ const selection = await getCodemodSelections();
+
+ const finalConfirmation = await getConfirmation(selection);
+
+ if (!finalConfirmation) {
+ return;
+ }
+
+ runCodemods(selection);
+ } catch (error: unknown) {
+ if (error instanceof Error && error.name === "ExitPromptError") {
+ // user exited, do nothing
+ } else {
+ throw error;
+ }
+ }
+}
+
+main();
diff --git a/component-catalog.json b/component-catalog.json
new file mode 100644
index 000000000..7a4f46cab
--- /dev/null
+++ b/component-catalog.json
@@ -0,0 +1,860 @@
+{
+ "meta": {
+ "packageName": "@lifesg/react-design-system",
+ "totalModules": 75
+ },
+ "components": [
+ {
+ "name": "Accordion",
+ "importPath": "@lifesg/react-design-system/accordion",
+ "description": "Props for the Accordion component - collapsible content container. Wraps one or more `Accordion.Item` children in a panel that can be individually expanded or collapsed. Supports a header title and an expand/collapse-all control.",
+ "searchKeys": [
+ "collapsible",
+ "disclosure",
+ "expand collapse",
+ "expandable",
+ "panel"
+ ]
+ },
+ {
+ "name": "Alert",
+ "importPath": "@lifesg/react-design-system/alert",
+ "description": "Props for the Alert component - contextual feedback banner. Displays an inline message with an optional icon, action link, and configurable severity type and size.",
+ "searchKeys": [
+ "banner",
+ "info box",
+ "notification",
+ "status message",
+ "warning message"
+ ]
+ },
+ {
+ "name": "Animations",
+ "importPath": "@lifesg/react-design-system/animations",
+ "description": "",
+ "searchKeys": []
+ },
+ {
+ "name": "BaseTheme",
+ "importPath": "@lifesg/react-design-system/theme",
+ "description": "for color customisation, can specify subset of set",
+ "searchKeys": [
+ "brand theme",
+ "color scheme",
+ "design tokens config",
+ "ThemeProvider",
+ "theming"
+ ]
+ },
+ {
+ "name": "BoxContainer",
+ "importPath": "@lifesg/react-design-system/box-container",
+ "description": "Props for the BoxContainer component - collapsible titled card. Renders a bordered box with a header title and optionally a status icon, call-to-action element, and expand/collapse toggle.",
+ "searchKeys": [
+ "bordered box",
+ "collapsible card",
+ "expandable section",
+ "titled panel"
+ ]
+ },
+ {
+ "name": "Breadcrumb",
+ "importPath": "@lifesg/react-design-system/breadcrumb",
+ "description": "Props for the Breadcrumb component - horizontal navigation trail. Renders an ordered list of anchor links representing the current page hierarchy. When links overflow, a fade gradient is applied at the scrollable edges.",
+ "searchKeys": [
+ "crumb nav",
+ "hierarchy links",
+ "navigation trail",
+ "page path"
+ ]
+ },
+ {
+ "name": "Button",
+ "importPath": "@lifesg/react-design-system/button",
+ "description": "Props for the Button component - primary call-to-action element Use buttons to trigger immediate actions like form submissions, dialog confirmations, or navigation. Choose the appropriate style type based on action hierarchy. The button comes in two sizes: Default and Small, available via Button.Default and Button.Small.",
+ "searchKeys": [
+ "action trigger",
+ "click handler",
+ "CTA",
+ "primary button",
+ "submit"
+ ]
+ },
+ {
+ "name": "Calendar",
+ "importPath": "@lifesg/react-design-system/calendar",
+ "description": "Props for the Calendar component - standalone date picker. Renders a full calendar view for selecting a single date. Extends `CommonCalendarProps` which provides min/max dates, disabled dates, and locale settings.",
+ "searchKeys": [
+ "date grid",
+ "date picker",
+ "date selector",
+ "day picker",
+ "monthly view"
+ ]
+ },
+ {
+ "name": "Card",
+ "importPath": "@lifesg/react-design-system/card",
+ "description": "Props for the Card component - elevated content container. Renders a styled box with a white background and rounded corners. Inherits all `HTMLDivElement` attributes.",
+ "searchKeys": ["content box", "panel", "paper", "surface", "tile"]
+ },
+ {
+ "name": "Checkbox",
+ "importPath": "@lifesg/react-design-system/checkbox",
+ "description": "Props for the Checkbox component - binary selection control. Renders a styled checkbox with optional indeterminate state. Inherits all standard `HTMLInputElement` attributes.",
+ "searchKeys": [
+ "boolean input",
+ "check mark",
+ "multi-select control",
+ "tick box"
+ ]
+ },
+ {
+ "name": "Color",
+ "importPath": "@lifesg/react-design-system/color",
+ "description": "",
+ "searchKeys": []
+ },
+ {
+ "name": "CountdownTimer",
+ "importPath": "@lifesg/react-design-system/countdown-timer",
+ "description": "Props for the CountdownTimer component - floating sticky countdown display. Renders a countdown clock that sticks to the viewport edge as the user scrolls. When the remaining time falls below `notifyTimer`, the `onTick` and `onNotify` callbacks fire. Use `show` to start or pause the timer.",
+ "searchKeys": [
+ "clock",
+ "count down",
+ "expiry timer",
+ "session timeout",
+ "time remaining"
+ ]
+ },
+ {
+ "name": "DataTable",
+ "importPath": "@lifesg/react-design-system/data-table",
+ "description": "Props for the DataTable component - tabular data display with selection. DataTable organises row data into columns and supports sortable headers, optional row selection, loading states, sticky headers, and empty states. Use it when users need to scan, compare, and act on structured data.",
+ "searchKeys": [
+ "data grid",
+ "grid",
+ "sortable table",
+ "spreadsheet",
+ "table"
+ ]
+ },
+ {
+ "name": "DateInput",
+ "importPath": "@lifesg/react-design-system/date-input",
+ "description": "Props for the DateInput component - single date picker with calendar overlay. Displays a text input that opens an inline calendar for selecting a single date. Controlled via a `\"YYYY-MM-DD\"` string value. Supports optional confirmation buttons, disabled and read-only modes, and focus/blur callbacks.",
+ "searchKeys": [
+ "calendar input",
+ "date entry",
+ "date field",
+ "date selector",
+ "single date"
+ ]
+ },
+ {
+ "name": "DateRangeInput",
+ "importPath": "@lifesg/react-design-system/date-range-input",
+ "description": "Props for the DateRangeInput component - date range picker with calendar overlay. Displays two linked text inputs (start and end) that open a shared calendar for selecting a date range. Controlled via `\"YYYY-MM-DD\"` string values. Supports week, month, and fixed-day range variants in addition to the default free-range mode.",
+ "searchKeys": [
+ "date range",
+ "date span",
+ "from to date",
+ "period picker",
+ "start end date"
+ ]
+ },
+ {
+ "name": "DesignToken",
+ "importPath": "@lifesg/react-design-system/design-token",
+ "description": "",
+ "searchKeys": []
+ },
+ {
+ "name": "Divider",
+ "importPath": "@lifesg/react-design-system/divider",
+ "description": "Props for the Divider component - horizontal rule separator. Renders a styled horizontal rule with configurable thickness, line style, color, and optional grid column span (via `ColDiv` layout props).",
+ "searchKeys": [
+ "horizontal rule",
+ "hr",
+ "line break",
+ "section divider",
+ "separator"
+ ]
+ },
+ {
+ "name": "Drawer",
+ "importPath": "@lifesg/react-design-system/drawer",
+ "description": "Props for the Drawer component - slide-in side panel. Renders a panel that slides in from the side of the screen, overlaying the page content. Toggled via the `show` prop and dismissed via the close button or overlay click callbacks.",
+ "searchKeys": [
+ "flyout",
+ "off-canvas",
+ "side panel",
+ "sidebar",
+ "slide out"
+ ]
+ },
+ {
+ "name": "ErrorDisplay",
+ "importPath": "@lifesg/react-design-system/error-display",
+ "description": "Base content attributes for the ErrorDisplay component. Shared between `ErrorDisplayProps` and the DataTable `emptyView` override.",
+ "searchKeys": [
+ "404 page",
+ "empty state",
+ "error page",
+ "maintenance screen",
+ "not found"
+ ]
+ },
+ {
+ "name": "FeedbackRating",
+ "importPath": "@lifesg/react-design-system/feedback-rating",
+ "description": "Props for the FeedbackRating component - star-rating survey widget. Renders an optional banner image, a description prompt, a row of star buttons, and a submit button. Controlled via the `rating` prop.",
+ "searchKeys": [
+ "NPS",
+ "rating widget",
+ "review",
+ "satisfaction score",
+ "star rating"
+ ]
+ },
+ {
+ "name": "FileUpload",
+ "importPath": "@lifesg/react-design-system/file-upload",
+ "description": "Props for an individual file item displayed in the upload list.",
+ "searchKeys": [
+ "attachment",
+ "drag and drop",
+ "drop zone",
+ "file input",
+ "upload field"
+ ]
+ },
+ {
+ "name": "Filter",
+ "importPath": "@lifesg/react-design-system/filter",
+ "description": "Props for the Filter component - collapsible filter panel. Renders a panel of `Filter.Item` children with a header title, clear button, and mobile-specific dismiss/done controls.",
+ "searchKeys": [
+ "facet",
+ "filter drawer",
+ "refinement panel",
+ "search filter",
+ "sidebar filter"
+ ]
+ },
+ {
+ "name": "Footer",
+ "importPath": "@lifesg/react-design-system/footer",
+ "description": "Any custom attributes you would like to pass to the link",
+ "searchKeys": [
+ "bottom bar",
+ "copyright bar",
+ "footer nav",
+ "page footer links",
+ "site footer"
+ ]
+ },
+ {
+ "name": "Form",
+ "importPath": "@lifesg/react-design-system/form",
+ "description": "Configuration for an informational addon attached to a Form label. Renders as a tooltip or popover triggered by an icon next to the label, providing supplementary context without cluttering the form field.",
+ "searchKeys": [
+ "field group",
+ "form field",
+ "form layout",
+ "input wrapper",
+ "label input pair"
+ ]
+ },
+ {
+ "name": "FullscreenImageCarousel",
+ "importPath": "@lifesg/react-design-system/fullscreen-image-carousel",
+ "description": "Props for the FullscreenImageCarousel component - fullscreen image gallery modal. Renders a modal overlay with a swipeable image carousel and optional thumbnail strip. Extends key `ModalProps` fields for visibility and animation.",
+ "searchKeys": [
+ "gallery modal",
+ "image gallery",
+ "image slider",
+ "lightbox",
+ "photo viewer"
+ ]
+ },
+ {
+ "name": "HistogramSlider",
+ "importPath": "@lifesg/react-design-system/histogram-slider",
+ "description": "Represents a single bin in the histogram distribution chart.",
+ "searchKeys": [
+ "bar chart slider",
+ "density slider",
+ "distribution chart",
+ "price range filter",
+ "range histogram"
+ ]
+ },
+ {
+ "name": "IconButton",
+ "importPath": "@lifesg/react-design-system/icon-button",
+ "description": "Props for the IconButton component - icon-only button. Renders a square button containing only an icon child. Extends all `HTMLButtonElement` attributes. Choose the style type based on hierarchy and the size type based on available space.",
+ "searchKeys": [
+ "ghost button",
+ "icon action",
+ "square button",
+ "symbol button",
+ "toolbar button"
+ ]
+ },
+ {
+ "name": "ImageButton",
+ "importPath": "@lifesg/react-design-system/image-button",
+ "description": "Props for the ImageButton component - image thumbnail button. Renders a clickable image tile with selected and error visual states. Extends all `HTMLButtonElement` attributes.",
+ "searchKeys": [
+ "image picker",
+ "image tile",
+ "photo button",
+ "picture button",
+ "thumbnail selector"
+ ]
+ },
+ {
+ "name": "Input",
+ "importPath": "@lifesg/react-design-system/input",
+ "description": "Props for the Input component - single-line text entry field. Extends all standard HTML `` attributes. Use for free-text, numeric, email, telephone, or search entry in forms. Supports optional clear button, error state, and a borderless style variant.",
+ "searchKeys": [
+ "search field",
+ "single line input",
+ "text box",
+ "text entry",
+ "text field"
+ ]
+ },
+ {
+ "name": "InputGroup",
+ "importPath": "@lifesg/react-design-system/input-group",
+ "description": "The type of addon attached to the InputGroup. - `\"label\"`: Fixed text prefix or suffix (e.g., currency symbol, unit). - `\"list\"`: Selectable dropdown addon (e.g., country-code selector). - `\"custom\"`: Arbitrary JSX content as the addon.",
+ "searchKeys": [
+ "addon input",
+ "currency input",
+ "inline addon",
+ "prefixed input",
+ "suffixed input"
+ ]
+ },
+ {
+ "name": "InputMultiSelect",
+ "importPath": "@lifesg/react-design-system/input-multi-select",
+ "description": "Props for the InputMultiSelect component - multi-option dropdown selector. Allows the user to choose multiple options from a dropdown list with checkboxes. Supports async option loading, search, and controlled selected state. For single-option selection use `InputSelect`.",
+ "searchKeys": [
+ "checkbox dropdown",
+ "multi option picker",
+ "multi-select dropdown",
+ "multiple choice",
+ "tag select"
+ ]
+ },
+ {
+ "name": "InputNestedMultiSelect",
+ "importPath": "@lifesg/react-design-system/input-nested-multi-select",
+ "description": "Props for the InputNestedMultiSelect component - multi-option hierarchical dropdown. Allows the user to drill into nested categories and select multiple values from a three-level hierarchy (L1 → L2 → L3). Use `InputNestedSelect` for single value selection. Supports search and async loading.",
+ "searchKeys": [
+ "cascading checklist",
+ "hierarchical multi-select",
+ "nested dropdown multiple",
+ "tree select multiple"
+ ]
+ },
+ {
+ "name": "InputNestedSelect",
+ "importPath": "@lifesg/react-design-system/input-nested-select",
+ "description": "Props for the InputNestedSelect component - single-option hierarchical dropdown. Allows the user to drill into nested categories to select a single value from a three-level hierarchy (L1 → L2 → L3). Use `InputNestedMultiSelect` for selecting multiple values. Supports search and async loading.",
+ "searchKeys": [
+ "cascading dropdown",
+ "category picker",
+ "hierarchical select",
+ "nested category",
+ "tree select"
+ ]
+ },
+ {
+ "name": "InputRangeSelect",
+ "importPath": "@lifesg/react-design-system/input-range-select",
+ "description": "A generic from/to pair used by InputRangeSelect for options and selected values.",
+ "searchKeys": [
+ "dual select",
+ "from to dropdown",
+ "price range select",
+ "range selector",
+ "start end picker"
+ ]
+ },
+ {
+ "name": "InputRangeSlider",
+ "importPath": "@lifesg/react-design-system/input-range-slider",
+ "description": "Shared configuration props used by both `InputSlider` and `InputRangeSlider`.",
+ "searchKeys": [
+ "dual thumb slider",
+ "min max slider",
+ "price range slider",
+ "range slider",
+ "two handle slider"
+ ]
+ },
+ {
+ "name": "InputSelect",
+ "importPath": "@lifesg/react-design-system/input-select",
+ "description": "The list of options to display in the dropdown.",
+ "searchKeys": [
+ "combobox",
+ "dropdown",
+ "option list",
+ "picker",
+ "select box"
+ ]
+ },
+ {
+ "name": "InputSlider",
+ "importPath": "@lifesg/react-design-system/input-slider",
+ "description": "Props for the InputSlider component - single-thumb numeric slider. Renders a horizontal slider with a single thumb for selecting a numeric value within a given range. Supports step intervals, track colour customisation, and value labels. For a dual-thumb range slider use `InputRangeSlider`.",
+ "searchKeys": [
+ "knob",
+ "numeric slider",
+ "range input",
+ "scrubber",
+ "single handle slider"
+ ]
+ },
+ {
+ "name": "Layout",
+ "importPath": "@lifesg/react-design-system/layout",
+ "description": "Shared layout props for all layout container components.",
+ "searchKeys": [
+ "column layout",
+ "flex layout",
+ "grid container",
+ "page wrapper",
+ "responsive container"
+ ]
+ },
+ {
+ "name": "LinkList",
+ "importPath": "@lifesg/react-design-system/link-list",
+ "description": "Props for the LinkList component - list of labelled hyperlinks. Renders a vertical list of titled link items with optional descriptions. If `maxShown` is set, extra items collapse behind a \"Show more\" toggle.",
+ "searchKeys": [
+ "hyperlink list",
+ "link group",
+ "nav list",
+ "resource links",
+ "show more list"
+ ]
+ },
+ {
+ "name": "MaskedInput",
+ "importPath": "@lifesg/react-design-system/masked-input",
+ "description": "The async load state for a MaskedInput in read-only mode. - `\"loading\"`: Shows a loading indicator. - `\"fail\"`: Shows an error state with a retry action. - `\"success\"`: Value is loaded and displayed.",
+ "searchKeys": [
+ "hide value",
+ "mask unmask",
+ "redacted input",
+ "reveal toggle",
+ "sensitive field"
+ ]
+ },
+ {
+ "name": "Masonry",
+ "importPath": "@lifesg/react-design-system/masonry",
+ "description": "number of columns on desktop resolutions",
+ "searchKeys": [
+ "brick layout",
+ "pinterest grid",
+ "responsive columns",
+ "variable height grid",
+ "waterfall layout"
+ ]
+ },
+ {
+ "name": "Masthead",
+ "importPath": "@lifesg/react-design-system/masthead",
+ "description": "Note: Had to use this method because we had to create a custom type to handle Web Components (refer to this guide https://blog.devgenius.io/how-to-use-web-components-in-react-54c951399bfd) But we are having troubles exporting the custom type definition via rollup",
+ "searchKeys": []
+ },
+ {
+ "name": "Media",
+ "importPath": "@lifesg/react-design-system/media",
+ "description": "",
+ "searchKeys": []
+ },
+ {
+ "name": "Modal",
+ "importPath": "@lifesg/react-design-system/modal",
+ "description": "Props for the Modal component - animated overlay dialog. Renders content inside a portal injected into the DOM. Supports directional slide-in animation, optional overlay-click dismissal, and custom z-index for stacked modals. Toggle visibility with the `show` prop.",
+ "searchKeys": [
+ "confirmation dialog",
+ "dialog",
+ "lightbox",
+ "overlay dialog",
+ "popup"
+ ]
+ },
+ {
+ "name": "NavbarHeight",
+ "importPath": "@lifesg/react-design-system/navbar",
+ "description": "Navbar action buttons for mobile drawer. Takes desktop if not specified",
+ "searchKeys": [
+ "app bar",
+ "header nav",
+ "menu bar",
+ "navigation header",
+ "top navigation"
+ ]
+ },
+ {
+ "name": "NotificationBanner",
+ "importPath": "@lifesg/react-design-system/notification-banner",
+ "description": "Props for the NotificationBanner component - dismissible top-of-page banner. Renders a full-width banner with optional sticky positioning and a dismiss button. Toggle visibility with the `visible` prop.",
+ "searchKeys": [
+ "alert banner",
+ "announcement bar",
+ "info bar",
+ "system message",
+ "top banner"
+ ]
+ },
+ {
+ "name": "OtpInput",
+ "importPath": "@lifesg/react-design-system/otp-input",
+ "description": "Props for the OtpInput component - one-time password entry field. Renders a row of individual character inputs for OTP entry, with a configurable submit button and a cooldown timer that re-disables the button after each submission.",
+ "searchKeys": [
+ "6-digit input",
+ "one-time password",
+ "PIN entry",
+ "SMS code",
+ "verification code"
+ ]
+ },
+ {
+ "name": "Overlay",
+ "importPath": "@lifesg/react-design-system/overlay",
+ "description": "Props for the Overlay component - backdrop overlay portal. Renders a semi-transparent backdrop injected into the DOM that can optionally blur the content behind it. Used as a layer beneath modals, drawers, and other floating UI elements.",
+ "searchKeys": [
+ "backdrop",
+ "background layer",
+ "darkened background",
+ "dimmer",
+ "scrim"
+ ]
+ },
+ {
+ "name": "Pagination",
+ "importPath": "@lifesg/react-design-system/pagination",
+ "description": "Props for the Pagination component - page navigation control. Renders page number buttons and optional first/last navigation, with an optional page-size dropdown. Requires `totalItems` and `activePage` to be controlled by the parent.",
+ "searchKeys": [
+ "next previous",
+ "page numbers",
+ "pager",
+ "paging control",
+ "results navigation"
+ ]
+ },
+ {
+ "name": "PhoneNumberInput",
+ "importPath": "@lifesg/react-design-system/phone-number-input",
+ "description": "A country entry used to populate the country-code dropdown.",
+ "searchKeys": [
+ "country code input",
+ "dialling code",
+ "international phone",
+ "mobile number",
+ "telephone field"
+ ]
+ },
+ {
+ "name": "Pill",
+ "importPath": "@lifesg/react-design-system/pill",
+ "description": "Props for the Pill component - compact label badge. Renders a small pill-shaped tag with a configurable display style (solid fill or outline) and color scheme. Inherits all `HTMLDivElement` attributes.",
+ "searchKeys": [
+ "badge",
+ "chip",
+ "colored label",
+ "label tag",
+ "status badge"
+ ]
+ },
+ {
+ "name": "Popover",
+ "importPath": "@lifesg/react-design-system/popover",
+ "description": "Props for the Popover component - floating content bubble. Renders a floating tooltip-style bubble positioned relative to its trigger element. Controlled via the `visible` prop.",
+ "searchKeys": [
+ "contextual overlay",
+ "floating hint",
+ "help popup",
+ "hover card",
+ "info bubble"
+ ]
+ },
+ {
+ "name": "PopoverV2",
+ "importPath": "@lifesg/react-design-system/popover-v2",
+ "description": "Props for the PopoverV2 component - floating content bubble (v2). The base popover bubble controlled via `visible`. For most use cases prefer `PopoverV2.Trigger` which handles positioning automatically.",
+ "searchKeys": [
+ "anchored tooltip",
+ "floating popover",
+ "floating-ui popover",
+ "info bubble v2",
+ "positioned overlay"
+ ]
+ },
+ {
+ "name": "PredictiveTextInput",
+ "importPath": "@lifesg/react-design-system/predictive-text-input",
+ "description": "Props for the PredictiveTextInput component - autocomplete text input. Displays suggestions fetched asynchronously as the user types. The suggestions list appears after a configurable minimum number of characters have been entered. Selecting a suggestion calls `onSelectOption`. Used for location search, name lookup, and other typeahead scenarios.",
+ "searchKeys": [
+ "autocomplete",
+ "autosuggest",
+ "live search",
+ "search suggestions",
+ "typeahead"
+ ]
+ },
+ {
+ "name": "ProgressIndicator",
+ "importPath": "@lifesg/react-design-system/progress-indicator",
+ "description": "Props for the ProgressIndicator component - step progress tracker. Renders a horizontal row of step labels with the current step highlighted. Supports a custom display extractor for non-string step items.",
+ "searchKeys": [
+ "form progress",
+ "multi-step progress",
+ "step tracker",
+ "stepper",
+ "wizard steps"
+ ]
+ },
+ {
+ "name": "RadioButton",
+ "importPath": "@lifesg/react-design-system/radio-button",
+ "description": "Props for the RadioButton component - single-choice input control. Renders a styled radio button. Inherits all standard `HTMLInputElement` attributes including `checked`, `disabled`, `value`, and `onChange`.",
+ "searchKeys": [
+ "choice input",
+ "exclusive select",
+ "option button",
+ "radio",
+ "single choice"
+ ]
+ },
+ {
+ "name": "Sidenav",
+ "importPath": "@lifesg/react-design-system/sidenav",
+ "description": "Props for the Sidenav component - vertical side navigation panel. Renders a fixed-left sidebar containing `Sidenav.Group` children with icon-labelled items and optional drawer sub-items.",
+ "searchKeys": [
+ "left navigation",
+ "navigation panel",
+ "side menu",
+ "sidebar menu",
+ "vertical nav"
+ ]
+ },
+ {
+ "name": "SmartAppBanner",
+ "importPath": "@lifesg/react-design-system/smart-app-banner",
+ "description": "Props for the SmartAppBanner component - mobile app download prompt. Renders a banner that prompts users to download the mobile app. Supports optional slide-down animation and a dismiss callback.",
+ "searchKeys": [
+ "app download prompt",
+ "app store banner",
+ "install banner",
+ "mobile app CTA",
+ "open in app"
+ ]
+ },
+ {
+ "name": "Tab",
+ "importPath": "@lifesg/react-design-system/tab",
+ "description": "Props for the Tab component - tabbed content panel. Renders a row of tab selectors and shows the content of the active tab. Can be used in uncontrolled mode (`initialActive`) or controlled mode (`currentActive`).",
+ "searchKeys": [
+ "content switcher",
+ "segmented view",
+ "tab strip",
+ "tabbed interface",
+ "tabbed panel"
+ ]
+ },
+ {
+ "name": "Tag",
+ "importPath": "@lifesg/react-design-system/tag",
+ "description": "Props for the Tag component - interactive label badge. Like `Pill` but supports an interactive state with pointer cursor and press effects. Inherits all `HTMLElement` attributes.",
+ "searchKeys": [
+ "category tag",
+ "chip",
+ "clickable badge",
+ "label badge",
+ "status chip"
+ ]
+ },
+ {
+ "name": "Text",
+ "importPath": "@lifesg/react-design-system/text",
+ "description": "Props for the Text component - typography primitives for the design system.",
+ "searchKeys": [
+ "body text",
+ "heading",
+ "label text",
+ "text style",
+ "typography"
+ ]
+ },
+ {
+ "name": "Textarea",
+ "importPath": "@lifesg/react-design-system/input-textarea",
+ "description": "Props for the InputTextarea component - multi-line text entry field. Extends all standard HTML `