diff --git a/codemods/deprecate-v2-tokens/data.ts b/codemods/deprecate-v2-tokens/data.ts index 5b70b64e8..5e65860e7 100644 --- a/codemods/deprecate-v2-tokens/data.ts +++ b/codemods/deprecate-v2-tokens/data.ts @@ -147,6 +147,10 @@ export const componentMap = [ oldName: "ColDivProps", newName: "V2_ColDivProps", }, + { + oldName: "TextList", + newName: "V2_TextList", + }, // Added theme name mappings { oldName: "BaseTheme", @@ -244,6 +248,10 @@ export const pathMap = [ oldPath: "layout", newPath: "v2_layout", }, + { + oldPath: "text-list", + newPath: "v2_text-list", + }, { oldPath: "theme", newPath: "v2_theme", diff --git a/codemods/migrate-text-list/data.ts b/codemods/migrate-text-list/data.ts new file mode 100644 index 000000000..acff08853 --- /dev/null +++ b/codemods/migrate-text-list/data.ts @@ -0,0 +1,16 @@ +export const sizePropMapping = { + D1: "header-xxl", + D2: "header-xl", + D3: "header-md", + D4: "header-sm", + H1: "header-lg", + H2: "header-md", + H3: "header-sm", + H4: "header-xs", + H5: "body-md", + H6: "body-sm", + DBody: "header-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..65d6b0aec --- /dev/null +++ b/codemods/migrate-text-list/index.ts @@ -0,0 +1,120 @@ +import { API, FileInfo, JSCodeshift } from "jscodeshift"; +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); + + let isV2TextListImport = false; + + source.find(j.ImportDeclaration).forEach((path) => { + const importPath = path.node.source.value; + + if ( + importPath === IMPORT_PATHS.V2_TEXT_LIST || + importPath === IMPORT_PATHS.DESIGN_SYSTEM + ) { + path.node.specifiers?.forEach((specifier) => { + if ( + j.ImportSpecifier.check(specifier) && + specifier.imported.name === IMPORT_SPECIFIERS.V2_TEXT_LIST + ) { + specifier.imported.name = IMPORT_SPECIFIERS.TEXT_LIST; + + if ( + specifier.local && + specifier.local.name === IMPORT_SPECIFIERS.V2_TEXT_LIST + ) { + specifier.local.name = IMPORT_SPECIFIERS.TEXT_LIST; + } + + if (importPath === IMPORT_PATHS.V2_TEXT_LIST) { + path.node.source.value = IMPORT_PATHS.TEXT_LIST; + } + + isV2TextListImport = true; + } + }); + } + }); + + if (isV2TextListImport) { + 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; + if (sizePropMapping[sizeValue]) { + attribute.value = j.literal( + sizePropMapping[sizeValue] + ); + } + } + }); + } + }); + } + + return source.toSource(); +} diff --git a/codemods/run-codemod.ts b/codemods/run-codemod.ts index c6af34f98..0fd735e90 100644 --- a/codemods/run-codemod.ts +++ b/codemods/run-codemod.ts @@ -14,6 +14,7 @@ enum Codemod { MigrateLayout = "migrate-layout", MigrateMediaQuery = "migrate-media-query", MigrateText = "migrate-text", + MigrateTextList = "migrate-text-list", } enum Theme { @@ -32,6 +33,8 @@ const CodemodDescriptions: { [key in Codemod]: string } = { [Codemod.MigrateMediaQuery]: "Replace V2 media queries with new Breakpoint tokens", [Codemod.MigrateText]: "Replace V2_Text with new Typography components", + [Codemod.MigrateTextList]: + "Replace V2_TextList with new Textlist components", }; const TargetDirectoryPaths = { diff --git a/src/accordion/accordion-item.style.tsx b/src/accordion/accordion-item.style.tsx index 3852975be..f04108515 100644 --- a/src/accordion/accordion-item.style.tsx +++ b/src/accordion/accordion-item.style.tsx @@ -1,11 +1,10 @@ import { ChevronUpIcon } from "@lifesg/react-icons/chevron-up"; import { animated } from "react-spring"; import styled, { css } from "styled-components"; -import { V2_Color } from "../v2_color"; -import { V2_MediaQuery } from "../v2_media"; import { ClickableIcon } from "../shared/clickable-icon"; -import { V2_Text } from "../v2_text/text"; -import { Transition } from "../transition"; +import { Border, Colour, Motion } from "../theme"; +import { MediaQuery } from "../theme"; +import { Typography } from "../typography"; // ============================================================================= // STYLE INTERFACE, transient props are denoted with $ @@ -19,11 +18,11 @@ interface StyleProps { // STYLING // ============================================================================= export const Container = styled.div` - background-color: ${V2_Color.Neutral[8]} !important; - border-top: 1px solid ${V2_Color.Neutral[6]}; + background-color: ${Colour.bg} !important; + border-top: ${Border["width-010"]} ${Border.solid} ${Colour.border}; padding: ${(props) => (props.$isCollapsed ? "0 0 1rem" : "0")}; - ${V2_MediaQuery.MaxWidth.mobileL} { + ${MediaQuery.MaxWidth.sm} { padding: ${(props) => props.$isCollapsed ? ".25rem 0 1.05rem" : "0.5rem 0"}; } @@ -40,16 +39,16 @@ export const TitleContainer = styled.div` const TITLE_STYLE = (isCollapsed?: boolean) => css` flex: 1; margin: 1rem 2rem ${isCollapsed ? 0.5 : 1}rem 0; - transition: ${Transition.Base}; + transition: all ${Motion["duration-250"]} ${Motion["ease-standard"]}; `; -export const Title = styled(V2_Text.H3)` +export const Title = styled(Typography.HeaderSM)` ${(props) => { return TITLE_STYLE(props.$isCollapsed); }} `; -export const TitleH4 = styled(V2_Text.H4)` +export const TitleH4 = styled(Typography.HeaderXS)` ${(props) => { return TITLE_STYLE(props.$isCollapsed); }} @@ -60,14 +59,14 @@ export const ExpandCollapseButton = styled(ClickableIcon)` width: 3.25rem; padding: 1rem; transform: rotate(${(props) => (props.$isCollapsed ? 0 : 180)}deg); - transition: transform 300ms ease-in-out; + transition: transform ${Motion["duration-250"]} ${Motion["ease-default"]}; margin: auto -1rem auto 0; `; export const ChevronIcon = styled(ChevronUpIcon)` height: 1.25rem; width: 1.25rem; - color: ${V2_Color.Primary}; + color: ${Colour["icon-primary"]}; `; export const Expandable = styled(animated.div)` @@ -78,7 +77,7 @@ export const DescriptionContainer = styled.div` display: inline-block; padding-right: 4rem; - ${V2_MediaQuery.MaxWidth.tablet} { + ${MediaQuery.MaxWidth.lg} { padding-right: 0; } `; diff --git a/src/accordion/accordion.style.tsx b/src/accordion/accordion.style.tsx index eea847280..96ee8b41c 100644 --- a/src/accordion/accordion.style.tsx +++ b/src/accordion/accordion.style.tsx @@ -1,16 +1,16 @@ import styled, { css } from "styled-components"; import { Button } from "../button"; -import { V2_Color } from "../v2_color"; -import { V2_MediaQuery } from "../v2_media"; -import { V2_Text } from "../v2_text/text"; import { TitleStyleProps, TitleWrapperStyleProps } from "./types"; +import { Border, MediaQuery } from "../theme"; +import { Colour } from "../theme"; +import { Typography } from "../typography"; // ============================================================================ // STYLING // ============================================================================= export const Content = styled.div` width: 100%; - border-bottom: 1px solid ${V2_Color.Neutral[6]}; + border-bottom: ${Border["width-010"]} ${Border.solid} ${Colour.border}; `; export const TitleWrapper = styled.div` @@ -20,14 +20,14 @@ export const TitleWrapper = styled.div` justify-content: flex-end; padding-bottom: 1rem; - ${V2_MediaQuery.MaxWidth.mobileL} { + ${MediaQuery.MaxWidth.sm} { justify-content: flex-end; } ${(props) => { if (!props.$showTitleInMobile && !props.$hasExpandAll) { return css` - ${V2_MediaQuery.MaxWidth.mobileL} { + ${MediaQuery.MaxWidth.sm} { display: none; } `; @@ -35,18 +35,18 @@ export const TitleWrapper = styled.div` }} `; -export const Title = styled(V2_Text.H2)` +export const Title = styled(Typography.HeaderMD)` display: flex; align-self: flex-start; flex: 1; - ${V2_MediaQuery.MaxWidth.mobileL} { + ${MediaQuery.MaxWidth.sm} { text-align: left; } ${(props) => { if (!props.$showInMobile) { return css` - ${V2_MediaQuery.MaxWidth.mobileL} { + ${MediaQuery.MaxWidth.sm} { display: none; visibility: hidden; } diff --git a/src/text-list/index.ts b/src/text-list/index.ts new file mode 100644 index 000000000..e63a52f3d --- /dev/null +++ b/src/text-list/index.ts @@ -0,0 +1,9 @@ +import { UnorderedList } from "./unordered-list"; +import { OrderedList } from "./ordered-list"; + +export const TextList = { + Ul: UnorderedList, + Ol: OrderedList, +}; + +export * from "./types"; diff --git a/src/text-list/ordered-list.tsx b/src/text-list/ordered-list.tsx new file mode 100644 index 000000000..45b4cb87b --- /dev/null +++ b/src/text-list/ordered-list.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { StyledOrderedList } from "./text-list.styles"; +import { OrderedListProps } from "./types"; + +export const OrderedList = ({ + size = "body-baseline", + children, + ...otherProps +}: OrderedListProps) => { + return ( + + {children} + + ); +}; diff --git a/src/text-list/text-list.styles.tsx b/src/text-list/text-list.styles.tsx new file mode 100644 index 000000000..921ad15ad --- /dev/null +++ b/src/text-list/text-list.styles.tsx @@ -0,0 +1,92 @@ +import styled, { css } from "styled-components"; +import { OrderedListProps, UnorderedListProps } from "./types"; +import { Colour, Font, MediaQuery } from "../theme"; + +const baseListStyle = (bottomMargin: number) => ` + margin-bottom: ${bottomMargin ? bottomMargin : 0}rem; +`; + +const BASE_MARGIN = 3; + +// ============================================================================= +// ORDERED LIST +// ============================================================================ +export const StyledOrderedList = styled.ol` + ${(props) => baseListStyle(props.bottomMargin)} + margin-left: ${BASE_MARGIN}rem; + + ${MediaQuery.MaxWidth.lg} { + margin-left: 2.5rem; + } + + // Counter matters + counter-reset: list; + + li { + ${(props) => Font[`${props.size}-regular`]} + position: relative; + color: ${Colour.text}; + } + + ${(props) => { + const { reversed } = props; + const counterType = props.counterType || "decimal"; + const counterSeparator = props.counterSeparator || ")"; + + return css` + li:before { + content: counter(list, ${counterType}) "${counterSeparator}"; + counter-increment: ${reversed ? "list -1" : "list"}; + position: absolute; + left: -2rem; + } + `; + }} + + ${(props) => { + const { reversed, start } = props; + + if (start) { + const resetValue = reversed ? start + 1 : start - 1; + return css` + counter-reset: list ${resetValue}; + `; + } + }} + + list-style-position: outside; + list-style-type: none; + + // nested lists styling + ol, + ul { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } + + ul > li:before { + content: ""; + } +`; + +// ============================================================================= +// UNORDERED LIST +// ============================================================================= +export const StyledUnorderedList = styled.ul` + ${(props) => baseListStyle(props.bottomMargin)} + margin-left: 2.5rem; + list-style-type: ${(props) => props.bulletType || "disc"}; + + li { + ${(props) => Font[`${props.size}-regular`]} + color: ${Colour.text}; + } + + counter-reset: list; + + ol, + ul { + margin-top: 0.5rem; + margin-bottom: 0.5rem; + } +`; diff --git a/src/text-list/types.ts b/src/text-list/types.ts new file mode 100644 index 000000000..b9d1e013e --- /dev/null +++ b/src/text-list/types.ts @@ -0,0 +1,27 @@ +import { TypographySizeType } from "../theme/font/types"; + +interface BaseListProps { + children: JSX.Element | JSX.Element[]; + bottomMargin?: number | undefined; + // size?: V2_TextSizeType | undefined; + size?: TypographySizeType | undefined; +} + +export type CounterType = "lower-alpha" | "decimal" | "lower-roman"; + +export interface OrderedListProps extends BaseListProps { + /** Values: "lower-alpha" | "decimal" | "lower-roman" */ + counterType?: CounterType | undefined; + counterSeparator?: string | undefined; + /** Specifies if the list counter decrements */ + reversed?: boolean | undefined; + /** The value to start the list count from */ + start?: number | undefined; +} + +export type BulletType = "disc" | "circle" | "square" | "none"; + +export interface UnorderedListProps extends BaseListProps { + /** Values: "disc" | "circle" | "square" | "none" */ + bulletType?: BulletType | undefined; +} diff --git a/src/text-list/unordered-list.tsx b/src/text-list/unordered-list.tsx new file mode 100644 index 000000000..d8b9c88a6 --- /dev/null +++ b/src/text-list/unordered-list.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { StyledUnorderedList } from "./text-list.styles"; +import { UnorderedListProps } from "./types"; + +export const UnorderedList = ({ + size = "body-baseline", + children, + ...otherProps +}: UnorderedListProps) => { + return ( + + {children} + + ); +}; diff --git a/tests/codemod/deprecate-version/test-data.ts b/tests/codemod/deprecate-version/test-data.ts index c767d6f90..4ed48d86c 100644 --- a/tests/codemod/deprecate-version/test-data.ts +++ b/tests/codemod/deprecate-version/test-data.ts @@ -8,6 +8,7 @@ import { DesignToken } from "@lifesg/react-design-system/design-token"; import { ColDiv } from "@lifesg/react-design-system/layout"; import {ContainerType} from "@lifesg/react-design-system/layout" import {BaseColorSet} from "@lifesg/react-design-system/layout" +import {TextList} from "@lifesg/react-design-system/text-list" const Container = styled.div\` color: \${DesignToken.Table.Cell.Primary}; @@ -28,6 +29,11 @@ const Component = () => (
Item 2
Item 3
+ +
  • First
  • +
  • Second
  • +
  • Third
  • +
    ); @@ -45,6 +51,7 @@ import { V2_DesignToken } from "@lifesg/react-design-system/v2_design-token"; import { V2_ColDiv } from "@lifesg/react-design-system/v2_layout"; import {V2_ContainerType} from "@lifesg/react-design-system/v2_layout" import {BaseColorSet} from "@lifesg/react-design-system/layout" +import {V2_TextList} from "@lifesg/react-design-system/v2_text-list" const Container = styled.div\` color: \${V2_DesignToken.Table.Cell.Primary}; @@ -65,6 +72,11 @@ const Component = () => (
    Item 2
    Item 3
    + +
  • First
  • +
  • Second
  • +
  • Third
  • +
    ); diff --git a/tests/codemod/migrate-text-list/test-data.ts b/tests/codemod/migrate-text-list/test-data.ts new file mode 100644 index 000000000..72b6abf55 --- /dev/null +++ b/tests/codemod/migrate-text-list/test-data.ts @@ -0,0 +1,29 @@ +export const inputCode = ` + +import { V2_TextList } from "@lifesg/react-design-system/v2_text-list"; + + + +
  • First
  • +
  • Second
  • +
  • Third
  • +
    ; +; + + +`; + +export const expectedOutputCode = ` + +import { TextList } from "@lifesg/react-design-system/text-list"; + + + +
  • First
  • +
  • Second
  • +
  • Third
  • +
    ; +; + + +`; diff --git a/tests/codemod/migrate-text-list/transformer.spec.tsx b/tests/codemod/migrate-text-list/transformer.spec.tsx new file mode 100644 index 000000000..4c00b4204 --- /dev/null +++ b/tests/codemod/migrate-text-list/transformer.spec.tsx @@ -0,0 +1,36 @@ +import { execSync } from "child_process"; +import * as fs from "fs"; +import * as path from "path"; +import { expectedOutputCode, inputCode } from "./test-data"; + +describe("Codemod Transformer for V2_TextList to TextList", () => { + const inputPath = path.join(__dirname, "input.tsx"); + const outputPath = path.join(__dirname, "output.tsx"); + + beforeAll(() => { + // create sample input file for testing + jest.resetAllMocks(); + fs.writeFileSync(inputPath, inputCode); + }); + + afterAll(() => { + // delete the files created for testing (comment this out to view files) + fs.unlinkSync(inputPath); + fs.unlinkSync(outputPath); + }); + + it("should transform V2_TextList components to TextList components", () => { + fs.copyFileSync(inputPath, outputPath); + + // Execute the jscodeshift command for the codemod + execSync( + `jscodeshift --parser=tsx -t ./codemods/migrate-text-list ${outputPath}` + ); + + // Check the transformed code + const transformedCode = fs.readFileSync(outputPath, "utf8"); + + // Compare the transformed code with the expected output + expect(transformedCode.trim()).toEqual(expectedOutputCode.trim()); + }); +});