diff --git a/src/utils/outputs/download.ts b/src/utils/outputs/download.ts index baaeec62..f84d8428 100644 --- a/src/utils/outputs/download.ts +++ b/src/utils/outputs/download.ts @@ -29,6 +29,14 @@ import { } from "./index.ts"; import { generateCustomColorClass } from "./web/custom-color-class.ts"; import { generateAndroidReadmeFile } from "./compose/readme.ts"; +import { generateSwiftUIColorFile, generateSwiftUIColorScheme } from "./swiftui/colors.ts"; +import { generateSwiftUIReadmeFile } from "./swiftui/readme.ts"; +import { generateSwiftUIDimensionsFile, generateSwiftUIDimensionsSchemeFile } from "./swiftui/dimensions.ts"; +import { generateSwiftUIDensityEnumFile } from "./swiftui/density.ts"; +import { generateSwiftUIDesignSystemThemeFile, generateSwiftUIThemeFile } from "./swiftui/theme.ts"; +import { designSystemName, generateStaticSwiftUIFiles } from "./swiftui/shared.ts"; +import { generateSwiftUIElevationsFile } from "./swiftui/elevation.ts"; +import { generateSwiftUIFontFamilyFile, generateSwiftUITypographyFile, generateSwiftUITypographySchemeFile } from "./swiftui/typography.ts"; const download = (fileName: string, file: Blob) => { const element = document.createElement("a"); @@ -107,6 +115,64 @@ export const downloadTheme = async ( ); zip.file(`${androidDataFolder}/Density.kt`, generateDensityEnumFile()); + // SwiftUI (iOS) + const iOSFileName = kebabCase(fileName); + + const iOSFolder: string = "swiftui"; + const iOSThemeBrandingName: string = kebabCase(theme.branding.name) + const iOSCoreFolder: string = `${iOSFolder}/core`; + const iOSThemeFolder: string = `${iOSFolder}/${iOSThemeBrandingName}`; + const iOSDataFolder: string = `${iOSThemeFolder}/data`; + zip.file( + `${iOSFolder}/README.md`, + generateSwiftUIReadmeFile(), + ); + zip.file( + `${iOSFolder}/${designSystemName}ColorScheme.swift`, + generateSwiftUIColorScheme(iOSFileName, speakingNames, allColors), + ); + zip.file( + `${iOSFolder}/${designSystemName}Dimensions.swift`, + generateSwiftUIDimensionsSchemeFile(iOSFileName), + ); + zip.file( + `${iOSFolder}/${designSystemName}Typography.swift`, + generateSwiftUITypographySchemeFile(iOSFileName), + ); + zip.file( + `${iOSFolder}/${designSystemName}Theme.swift`, + generateSwiftUIDesignSystemThemeFile(iOSThemeBrandingName), + ); + // iOS - Theme + zip.file( + `${iOSThemeFolder}/${iOSFileName}.swift`, + generateSwiftUIThemeFile(iOSThemeBrandingName), + ); + // iOS - Theme - Date + zip.file( + `${iOSDataFolder}/${iOSThemeBrandingName}Dimensions.swift`, + generateSwiftUIDimensionsFile(iOSThemeBrandingName, theme), + ); + zip.file( + `${iOSDataFolder}/${iOSThemeBrandingName}Typography.swift`, + generateSwiftUITypographyFile(iOSThemeBrandingName, theme), + ); + zip.file( + `${iOSDataFolder}/${iOSThemeBrandingName}Colors.swift`, + generateSwiftUIColorFile(iOSThemeBrandingName, allColors, luminanceSteps), + ); + // iOS - Core + zip.file( + `${iOSCoreFolder}/AdaptiveColors+Descriptive.swift`, + generateStaticSwiftUIFiles() + ); + zip.file(`${iOSCoreFolder}/Fonts.swift`, generateSwiftUIFontFamilyFile()); + zip.file( + `${iOSCoreFolder}/Elevations.swift`, + generateSwiftUIElevationsFile(theme.elevation), + ); + zip.file(`${iOSCoreFolder}/Density.swift`, generateSwiftUIDensityEnumFile()); + // Utils const utilsFolder: string = "Utils"; zip.file( diff --git a/src/utils/outputs/swiftui/AdaptiveColors+Descripitve.swift b/src/utils/outputs/swiftui/AdaptiveColors+Descripitve.swift new file mode 100644 index 00000000..d3e23941 --- /dev/null +++ b/src/utils/outputs/swiftui/AdaptiveColors+Descripitve.swift @@ -0,0 +1,233 @@ +// +// Copyright 2024 by DB Systel GmbH +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + + +import SwiftUI + +public struct BasicColor { + public var text: TextColor + public var icon: IconColor + public var border: BorderColor + public var background: BackgroundColor +} + +public struct InvertedBackgroundColor { + public var contrastMax: StateColor + public var contrastHigh: StateColor + public var contrastLow: StateColor +} + +public struct InvertedColor { + public var background: InvertedBackgroundColor + public var onBackground: StateColor +} + +public struct OriginColor { + public var origin: StateColor + public var onOrigin: StateColor +} + +public struct TextColor { + public var `default`: StateColor + public var emphasis100: StateColor + public var emphasis90: StateColor + public var emphasis80: StateColor +} + +public struct IconColor { + public var `default`: StateColor + public var emphasis100: StateColor + public var emphasis90: StateColor + public var emphasis80: StateColor + public var emphasis70: StateColor +} + +public struct BorderColor { + public var `default`: StateColor + public var emphasis100: StateColor + public var emphasis70: StateColor + public var emphasis60: StateColor + public var emphasis50: StateColor +} + +public struct BackgroundColor { + public var transparent: TransparentColor + public var level1: StateColor + public var level2: StateColor + public var level3: StateColor +} + +public struct TransparentColor { + public var full: Color + public var semi: Color + public var hovered: Color + public var pressed: Color +} + +public struct StateColor { + public var `default`: Color + public var hovered: Color + public var pressed: Color +} + +extension DSColorVariant { + + public var basic: BasicColor { + .init( + text: .init( + default: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis100: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis90: .init( + default: onBgBasicEmphasis80Default, + hovered: onBgBasicEmphasis80Hovered, + pressed: onBgBasicEmphasis80Pressed + ), + emphasis80: .init( + default: onBgBasicEmphasis80Default, + hovered: onBgBasicEmphasis80Hovered, + pressed: onBgBasicEmphasis80Pressed + ) + ), + icon: .init( + default: .init( + default: onBgBasicEmphasis70Default, + hovered: onBgBasicEmphasis70Hovered, + pressed: onBgBasicEmphasis70Pressed + ), + emphasis100: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis90: .init( + default: onBgBasicEmphasis90Default, + hovered: onBgBasicEmphasis90Hovered, + pressed: onBgBasicEmphasis90Pressed + ), + emphasis80: .init( + default: onBgBasicEmphasis80Default, + hovered: onBgBasicEmphasis80Hovered, + pressed: onBgBasicEmphasis80Pressed + ), + emphasis70: .init( + default: onBgBasicEmphasis70Default, + hovered: onBgBasicEmphasis70Hovered, + pressed: onBgBasicEmphasis70Pressed + ) + ), + border: .init( + default: .init( + default: onBgBasicEmphasis60Default, + hovered: onBgBasicEmphasis60Hovered, + pressed: onBgBasicEmphasis60Pressed + ), + emphasis100: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis70: .init( + default: onBgBasicEmphasis70Default, + hovered: onBgBasicEmphasis70Hovered, + pressed: onBgBasicEmphasis70Pressed + ), + emphasis60: .init( + default: onBgBasicEmphasis60Default, + hovered: onBgBasicEmphasis60Hovered, + pressed: onBgBasicEmphasis60Pressed + ), + emphasis50: .init( + default: onBgBasicEmphasis50Default, + hovered: onBgBasicEmphasis50Hovered, + pressed: onBgBasicEmphasis50Pressed + ) + ), + background: .init( + transparent: .init( + full: bgBasicTransparentFullDefault, + semi: bgBasicTransparentSemiDefault, + hovered: bgBasicTransparentHovered, + pressed: bgBasicTransparentPressed + ), + level1: .init( + default: bgBasicLevel1Default, + hovered: bgBasicLevel1Hovered, + pressed: bgBasicLevel1Pressed + ), + level2: .init( + default: bgBasicLevel2Default, + hovered: bgBasicLevel2Hovered, + pressed: bgBasicLevel2Pressed + ), + level3: .init( + default: bgBasicLevel3Default, + hovered: bgBasicLevel3Hovered, + pressed: bgBasicLevel3Pressed + ) + ) + ) + } + + public var inverted: InvertedColor { + .init( + background: .init( + contrastMax: .init( + default: bgInvertedContrastMaxDefault, + hovered: bgInvertedContrastMaxHovered, + pressed: bgInvertedContrastMaxPressed + ), + contrastHigh: .init( + default: bgInvertedContrastHighDefault, + hovered: bgInvertedContrastHighHovered, + pressed: bgInvertedContrastHighPressed + ), + contrastLow: .init( + default: bgInvertedContrastLowDefault, + hovered: bgInvertedContrastLowHovered, + pressed: bgInvertedContrastLowPressed + ) + ), + onBackground: .init( + default: onBgInvertedDefault, + hovered: onBgInvertedHovered, + pressed: onBgInvertedPressed + ) + ) + } + + public var origin: OriginColor { + .init( + origin: .init( + default: originDefault, + hovered: originHovered, + pressed: originPressed + ), + onOrigin: .init( + default: onOriginDefault, + hovered: onOriginHovered, + pressed: onOriginPressed + ) + ) + } +} diff --git a/src/utils/outputs/swiftui/colors.ts b/src/utils/outputs/swiftui/colors.ts new file mode 100644 index 00000000..59f1638d --- /dev/null +++ b/src/utils/outputs/swiftui/colors.ts @@ -0,0 +1,215 @@ +import { DefaultColorType, HeisslufType, SpeakingName } from "../../data.ts"; +import { kebabCase } from "../../index.ts"; +import { getPalette } from "../index.ts"; +import { FALLBACK_COLOR } from "../../../constants.ts"; +import { designSystemName } from "./shared.ts"; + +const originAdditionalColors = [ + { name: "onOriginDefault", light: 0, dark: 0 }, + { name: "onOriginHovered", light: 0, dark: 0 }, + { name: "onOriginPressed", light: 0, dark: 0 }, + { name: "originDefault", light: 0, dark: 0 }, + { name: "originHovered", light: 0, dark: 0 }, + { name: "originPressed", light: 0, dark: 0 }, +]; + +const getSwiftUIColorFromHex = (hex: string = FALLBACK_COLOR): string => { + return `Color(hex: 0x${hex.replace("#", "")})`; +}; + +export const generateSwiftUIColorFile = ( + fileName: string, + allColors: Record, + luminanceSteps: number[], +): string => { + let resolvedTokenFile: string = `import SwiftUI + +let ${fileName}Colors: [String: Color] = [ +`; + + const palette: Record = getPalette( + allColors, + luminanceSteps, + ); + Object.entries(allColors).forEach(([name, color]) => { + const hslType = palette[name]; + hslType.forEach((hsl) => { + const key = `${name}${hsl.index}`; + resolvedTokenFile += ` "${key}": ${getSwiftUIColorFromHex(hsl.hex)},\n`; + }); + + resolvedTokenFile += ` "${name}Origin": ${getSwiftUIColorFromHex(color.origin)},\n`; + + resolvedTokenFile += ` "${name}OnOriginDefaultLight": ${getSwiftUIColorFromHex(color.onOriginLight)},\n`; + resolvedTokenFile += ` "${name}OnOriginHoveredLight": ${getSwiftUIColorFromHex(color.onOriginLightHovered)},\n`; + resolvedTokenFile += ` "${name}OnOriginPressedLight": ${getSwiftUIColorFromHex(color.onOriginLightPressed)},\n`; + resolvedTokenFile += ` "${name}OriginDefaultLight": ${getSwiftUIColorFromHex(color.originLight)},\n`; + resolvedTokenFile += ` "${name}OriginHoveredLight": ${getSwiftUIColorFromHex(color.originLightHovered)},\n`; + resolvedTokenFile += ` "${name}OriginPressedLight": ${getSwiftUIColorFromHex(color.originLightPressed)},\n`; + + resolvedTokenFile += ` "${name}OnOriginDefaultDark": ${getSwiftUIColorFromHex(color.onOriginDark)},\n`; + resolvedTokenFile += ` "${name}OnOriginHoveredDark": ${getSwiftUIColorFromHex(color.onOriginDarkHovered)},\n`; + resolvedTokenFile += ` "${name}OnOriginPressedDark": ${getSwiftUIColorFromHex(color.onOriginDarkPressed)},\n`; + resolvedTokenFile += ` "${name}OriginDefaultDark": ${getSwiftUIColorFromHex(color.originDark)},\n`; + resolvedTokenFile += ` "${name}OriginHoveredDark": ${getSwiftUIColorFromHex(color.originDarkHovered)},\n`; + resolvedTokenFile += ` "${name}OriginPressedDark": ${getSwiftUIColorFromHex(color.originDarkPressed)},\n`; + }); + + resolvedTokenFile = resolvedTokenFile.substring(0, resolvedTokenFile.lastIndexOf(',')); + resolvedTokenFile += `\n]\n`; + + return resolvedTokenFile; +}; + +const generateSwiftUIColorVariantExtension = ( + speakingNames: SpeakingName[], + resolvedScheme: string, + darkMode: boolean +): string => { + const colorScheme = kebabCase(darkMode ? "dark" : "light"); + + for (const speakingName of speakingNames) { + const color = `${name}${ + darkMode ? speakingName.dark : speakingName.light + }`; + const resolvedName = `${kebabCase(speakingName.name, true)}`; + if ( + speakingName.transparencyDark !== undefined || + speakingName.transparencyLight !== undefined + ) { + const transparency = + (speakingName.transparencyDark !== undefined + ? speakingName.transparencyDark + : speakingName.transparencyLight || 0) / 100; + resolvedScheme += ` self.${resolvedName} = colors["\\(colorName)${color}", default: .clear].opacity(${transparency})\n`; // DBColors.${color}.opacity(${transparency})\n`; + } else { + resolvedScheme += ` self.${resolvedName} = colors["\\(colorName)${color}", default: .clear]\n`; + } + } + + resolvedScheme += ` self.onOriginDefault = colors["\\(colorName)${name}${colorScheme}", default: .clear]\n`; + resolvedScheme += ` self.onOriginHovered = colors["\\(colorName)${name}OnOriginHovered${colorScheme}", default: .clear]\n`; + resolvedScheme += ` self.onOriginPressed = colors["\\(colorName)${name}OnOriginPressed${colorScheme}", default: .clear]\n`; + resolvedScheme += ` self.originDefault = colors["\\(colorName)${name}OriginDefault${colorScheme}", default: .clear]\n`; + resolvedScheme += ` self.originHovered = colors["\\(colorName)${name}OriginHovered${colorScheme}", default: .clear]\n`; + resolvedScheme += ` self.originPressed = colors["\\(colorName)${name}OriginPressed${colorScheme}", default: .clear]\n`; + return resolvedScheme; +}; + +const generateSwiftUIColorSchemeDarkLight = ( + fileName: string, + colorKeys: string[], + resolvedScheme: string, + darkMode?: boolean, +): string => { + console.log(fileName) + const colorScheme = kebabCase(darkMode ? "dark" : "light"); + + resolvedScheme += `\n static func getColorScheme${colorScheme}(colors: [String: Color]) -> ${designSystemName}ColorScheme {\n` + for (const name of colorKeys) { + resolvedScheme += ` + var ${name.toLowerCase()}Colors${colorScheme}: DSColorVariant { + .init(.${colorScheme.toLowerCase()}, colorName: "${name.toLowerCase()}", colors: colors) + }\n`; + } + resolvedScheme += ` + + return .init(\n`; + colorKeys.forEach((name, index) => { + resolvedScheme += ` ${name}: ${name}Colors${colorScheme}`; + // if not last element: + if (index < colorKeys.length - 1) { + resolvedScheme += `,\n` + } + }); + resolvedScheme += `\n )\n }\n`; + + return resolvedScheme; +}; + +export const generateSwiftUIColorScheme = ( + fileName: string, + speakingNames: SpeakingName[], + allColors: Record, +): string => { + const resolvedNames: Record = {}; + const colorKeys = Object.keys(allColors); + let resolvedScheme: string = `import SwiftUI + +extension Color { + init(hex: Int, opacity: Double = 1) { + self.init( + .sRGB, + red: Double((hex >> 16) & 0xff) / 255, + green: Double((hex >> 08) & 0xff) / 255, + blue: Double((hex >> 00) & 0xff) / 255, + opacity: opacity + ) + } +} + +`; + + // 1. Generate generic DSColorVariant protocol' + const name = colorKeys[0] + const allSpeakingNames = [...speakingNames, ...originAdditionalColors]; + resolvedScheme += `public struct DSColorVariant {\n`; + for (const speakingName of allSpeakingNames) { + const resolvedName = `${kebabCase(speakingName.name, true)}`; + resolvedNames[`${name}${speakingName.name}`] = resolvedName; + resolvedScheme += ` public let ${resolvedName}: Color\n`; + } + + resolvedScheme += `\n init(_ scheme: DSColorScheme, colorName: String, colors: [String: Color]) { + switch scheme { + case .dark: +`; + + resolvedScheme = generateSwiftUIColorVariantExtension( + speakingNames, + resolvedScheme, + true + ); + + resolvedScheme += `\n case .light:\n`; + + resolvedScheme = generateSwiftUIColorVariantExtension( + speakingNames, + resolvedScheme, + false + ); + + resolvedScheme += `\n }\n }\n}\n\n`; + + // 2. Generate ColorSchemes for semantic colors + resolvedScheme += `public struct ${designSystemName}ColorScheme {\n`; + for (const name of colorKeys) { + resolvedScheme += ` public let ${name}: DSColorVariant\n`; + } + + resolvedScheme = generateSwiftUIColorSchemeDarkLight( + fileName, + colorKeys, + resolvedScheme, + true, + ); + + resolvedScheme = generateSwiftUIColorSchemeDarkLight( + fileName, + colorKeys, + resolvedScheme, + false, + ); + + resolvedScheme += `\n}\n\n`; + + resolvedScheme += ` +enum DSColorScheme { + case light + case dark +} + +`; + + return resolvedScheme; +}; diff --git a/src/utils/outputs/swiftui/density.ts b/src/utils/outputs/swiftui/density.ts new file mode 100644 index 00000000..b7346961 --- /dev/null +++ b/src/utils/outputs/swiftui/density.ts @@ -0,0 +1,12 @@ +import { densities } from "./shared.ts"; + +export const generateSwiftUIDensityEnumFile = (): string => { + let resolvedString: string = "enum DSDensity: String {\n"; + + densities.forEach( density => { + resolvedString += ` case ${density.toLowerCase()} = "${density}"\n`; + }) + + resolvedString += "}\n"; + return resolvedString; +}; diff --git a/src/utils/outputs/swiftui/dimensions.ts b/src/utils/outputs/swiftui/dimensions.ts new file mode 100644 index 00000000..94e5397c --- /dev/null +++ b/src/utils/outputs/swiftui/dimensions.ts @@ -0,0 +1,265 @@ +import { ThemeType } from "../../data.ts"; +import traverse from "traverse"; +import { kebabCase } from "../../index.ts"; +import { + densities, + designSystemName, + devices, + shirtSizes, +} from "./shared.ts"; + +export const generateSwiftUIDimensionsFile = (fileName: string, theme: ThemeType): string => { + let resolvedTokenFile: string = ` +import SwiftUI + +struct ${fileName}Dimensions: DSDimensions { +`; + + traverse(theme).forEach(function (value) { + if ( + this.isLeaf && + this.path.length > 0 && + this.path[0] !== "branding" && + this.path[0] !== "colors" && + this.path[0] !== "additionalColors" && + this.path[0] !== "font" && + this.path[0] !== "transition" && + this.path[0] !== "elevation" && + this.path[0] !== "typography" && + !this.path.includes("desktop") && + !this.path.includes("_scale") + ) { + const key = `${kebabCase(this.path.join("-"), true)}`; + console.log(key) + console.log(value) + const finalValue = + typeof value === "string" || value instanceof String + ? `${Number(value) * 16 || `.nan`}` + : value; + + resolvedTokenFile += ` let ${key}: CGFloat = ${finalValue}\n`; + } + }); + + resolvedTokenFile += "}\n"; + + return resolvedTokenFile; +}; + +const dimensionTypes: Record = { + spacing: ["responsive", "fixed"], + sizing: ["base"], + border: ["height", "radius"], +}; + +export const generateSwiftUIDimensionsScheme = ( + fileName: string, + resolvedTokenFile: string, + density: string, + device: string, +): string => { + console.log(fileName) + + for (const [type, values] of Object.entries(dimensionTypes)) { + resolvedTokenFile += ` private static func get${kebabCase(type)}Dimensions${density}${device}(dimensions: DSDimensions) -> DS${kebabCase(type)}Dimensions {\n .init(\n`; + for (const value of values) { + const resolvedValue = value === "base" ? "" : `-${value}`; + const resolvedDevice = value === "responsive" ? `-${device}` : ""; + const resolvedDensity = type === "border" ? "" : `-${density}`; + + for (const size of shirtSizes) { + resolvedTokenFile += ` ${kebabCase(`${value}-${size}`, true)}: dimensions.${kebabCase(`${type}${resolvedValue}${resolvedDensity}${resolvedDevice}-${size}`, true)}`; + resolvedTokenFile += `,\n` + } + } + resolvedTokenFile = resolvedTokenFile.substring(0, resolvedTokenFile.lastIndexOf(',')); + resolvedTokenFile += `\n )\n }\n\n`; + } + + resolvedTokenFile += ` static func getDimensions${density}${device}(dimensions: DSDimensions) -> ${designSystemName}Dimensions {\n .init(\n`; + for (const type of Object.keys(dimensionTypes)) { + resolvedTokenFile += ` ${type}: get${kebabCase(type)}Dimensions${density}${device}(dimensions: dimensions),\n`; + } + resolvedTokenFile = resolvedTokenFile.substring(0, resolvedTokenFile.lastIndexOf(',')); + resolvedTokenFile += `\n )\n }\n\n`; + + return resolvedTokenFile; +}; + +export const generateSwiftUIDimensionsSchemeFile = (fileName: string): string => { + let resolvedTokenFile: string = ` +import SwiftUI + +protocol DSDimensions { + var spacingResponsiveRegularTablet3xs: CGFloat { get } + var spacingResponsiveRegularTablet2xs: CGFloat { get } + var spacingResponsiveRegularTabletXs: CGFloat { get } + var spacingResponsiveRegularTabletSm: CGFloat { get } + var spacingResponsiveRegularTabletMd: CGFloat { get } + var spacingResponsiveRegularTabletLg: CGFloat { get } + var spacingResponsiveRegularTabletXl: CGFloat { get } + var spacingResponsiveRegularTablet2xl: CGFloat { get } + var spacingResponsiveRegularTablet3xl: CGFloat { get } + var spacingResponsiveRegularMobile3xs: CGFloat { get } + var spacingResponsiveRegularMobile2xs: CGFloat { get } + var spacingResponsiveRegularMobileXs: CGFloat { get } + var spacingResponsiveRegularMobileSm: CGFloat { get } + var spacingResponsiveRegularMobileMd: CGFloat { get } + var spacingResponsiveRegularMobileLg: CGFloat { get } + var spacingResponsiveRegularMobileXl: CGFloat { get } + var spacingResponsiveRegularMobile2xl: CGFloat { get } + var spacingResponsiveRegularMobile3xl: CGFloat { get } + var spacingResponsiveFunctionalTablet3xs: CGFloat { get } + var spacingResponsiveFunctionalTablet2xs: CGFloat { get } + var spacingResponsiveFunctionalTabletXs: CGFloat { get } + var spacingResponsiveFunctionalTabletSm: CGFloat { get } + var spacingResponsiveFunctionalTabletMd: CGFloat { get } + var spacingResponsiveFunctionalTabletLg: CGFloat { get } + var spacingResponsiveFunctionalTabletXl: CGFloat { get } + var spacingResponsiveFunctionalTablet2xl: CGFloat { get } + var spacingResponsiveFunctionalTablet3xl: CGFloat { get } + var spacingResponsiveFunctionalMobile3xs: CGFloat { get } + var spacingResponsiveFunctionalMobile2xs: CGFloat { get } + var spacingResponsiveFunctionalMobileXs: CGFloat { get } + var spacingResponsiveFunctionalMobileSm: CGFloat { get } + var spacingResponsiveFunctionalMobileMd: CGFloat { get } + var spacingResponsiveFunctionalMobileLg: CGFloat { get } + var spacingResponsiveFunctionalMobileXl: CGFloat { get } + var spacingResponsiveFunctionalMobile2xl: CGFloat { get } + var spacingResponsiveFunctionalMobile3xl: CGFloat { get } + var spacingResponsiveExpressiveTablet3xs: CGFloat { get } + var spacingResponsiveExpressiveTablet2xs: CGFloat { get } + var spacingResponsiveExpressiveTabletXs: CGFloat { get } + var spacingResponsiveExpressiveTabletSm: CGFloat { get } + var spacingResponsiveExpressiveTabletMd: CGFloat { get } + var spacingResponsiveExpressiveTabletLg: CGFloat { get } + var spacingResponsiveExpressiveTabletXl: CGFloat { get } + var spacingResponsiveExpressiveTablet2xl: CGFloat { get } + var spacingResponsiveExpressiveTablet3xl: CGFloat { get } + var spacingResponsiveExpressiveMobile3xs: CGFloat { get } + var spacingResponsiveExpressiveMobile2xs: CGFloat { get } + var spacingResponsiveExpressiveMobileXs: CGFloat { get } + var spacingResponsiveExpressiveMobileSm: CGFloat { get } + var spacingResponsiveExpressiveMobileMd: CGFloat { get } + var spacingResponsiveExpressiveMobileLg: CGFloat { get } + var spacingResponsiveExpressiveMobileXl: CGFloat { get } + var spacingResponsiveExpressiveMobile2xl: CGFloat { get } + var spacingResponsiveExpressiveMobile3xl: CGFloat { get } + var spacingFixedRegular3xs: CGFloat { get } + var spacingFixedRegular2xs: CGFloat { get } + var spacingFixedRegularXs: CGFloat { get } + var spacingFixedRegularSm: CGFloat { get } + var spacingFixedRegularMd: CGFloat { get } + var spacingFixedRegularLg: CGFloat { get } + var spacingFixedRegularXl: CGFloat { get } + var spacingFixedRegular2xl: CGFloat { get } + var spacingFixedRegular3xl: CGFloat { get } + var spacingFixedFunctional3xs: CGFloat { get } + var spacingFixedFunctional2xs: CGFloat { get } + var spacingFixedFunctionalXs: CGFloat { get } + var spacingFixedFunctionalSm: CGFloat { get } + var spacingFixedFunctionalMd: CGFloat { get } + var spacingFixedFunctionalLg: CGFloat { get } + var spacingFixedFunctionalXl: CGFloat { get } + var spacingFixedFunctional2xl: CGFloat { get } + var spacingFixedFunctional3xl: CGFloat { get } + var spacingFixedExpressive3xs: CGFloat { get } + var spacingFixedExpressive2xs: CGFloat { get } + var spacingFixedExpressiveXs: CGFloat { get } + var spacingFixedExpressiveSm: CGFloat { get } + var spacingFixedExpressiveMd: CGFloat { get } + var spacingFixedExpressiveLg: CGFloat { get } + var spacingFixedExpressiveXl: CGFloat { get } + var spacingFixedExpressive2xl: CGFloat { get } + var spacingFixedExpressive3xl: CGFloat { get } + var sizingFixedMobileHeader: CGFloat { get } + var sizingRegular3xl: CGFloat { get } + var sizingRegular2xl: CGFloat { get } + var sizingRegularXl: CGFloat { get } + var sizingRegularLg: CGFloat { get } + var sizingRegularMd: CGFloat { get } + var sizingRegularSm: CGFloat { get } + var sizingRegularXs: CGFloat { get } + var sizingRegular2xs: CGFloat { get } + var sizingRegular3xs: CGFloat { get } + var sizingFunctional3xs: CGFloat { get } + var sizingFunctional2xs: CGFloat { get } + var sizingFunctionalXs: CGFloat { get } + var sizingFunctionalSm: CGFloat { get } + var sizingFunctionalMd: CGFloat { get } + var sizingFunctionalLg: CGFloat { get } + var sizingFunctionalXl: CGFloat { get } + var sizingFunctional2xl: CGFloat { get } + var sizingFunctional3xl: CGFloat { get } + var sizingExpressive3xs: CGFloat { get } + var sizingExpressive2xs: CGFloat { get } + var sizingExpressiveXs: CGFloat { get } + var sizingExpressiveSm: CGFloat { get } + var sizingExpressiveMd: CGFloat { get } + var sizingExpressiveLg: CGFloat { get } + var sizingExpressiveXl: CGFloat { get } + var sizingExpressive2xl: CGFloat { get } + var sizingExpressive3xl: CGFloat { get } + var borderHeight3xs: CGFloat { get } + var borderHeight2xs: CGFloat { get } + var borderHeightXs: CGFloat { get } + var borderHeightSm: CGFloat { get } + var borderHeightMd: CGFloat { get } + var borderHeightLg: CGFloat { get } + var borderHeightXl: CGFloat { get } + var borderHeight2xl: CGFloat { get } + var borderHeight3xl: CGFloat { get } + var borderRadius3xs: CGFloat { get } + var borderRadius2xs: CGFloat { get } + var borderRadiusXs: CGFloat { get } + var borderRadiusSm: CGFloat { get } + var borderRadiusMd: CGFloat { get } + var borderRadiusLg: CGFloat { get } + var borderRadiusXl: CGFloat { get } + var borderRadius2xl: CGFloat { get } + var borderRadius3xl: CGFloat { get } + var borderRadiusFull: CGFloat { get } +} + +`; + + for (const [type, values] of Object.entries(dimensionTypes)) { + resolvedTokenFile += `public struct DS${kebabCase(type)}Dimensions {\n`; + for (const value of values) { + for (const size of shirtSizes) { + // val fixedXl: Dp = Dimensions.spacingFixedXl, + resolvedTokenFile += ` public var ${kebabCase(`${value}-${size}`, true)}: CGFloat\n`; + } + } + resolvedTokenFile += "}\n\n"; + } + + resolvedTokenFile += `public struct ${designSystemName}Dimensions {\n`; + for (const type of Object.keys(dimensionTypes)) { + resolvedTokenFile += ` public var ${type}: DS${kebabCase(type)}Dimensions\n\n`; + } + + resolvedTokenFile += ` + init(spacing: DSSpacingDimensions, sizing: DSSizingDimensions, border: DSBorderDimensions) { + self.spacing = spacing + self.sizing = sizing + self.border = border + } + +` + + for (const density of densities) { + for (const device of devices) { + resolvedTokenFile = generateSwiftUIDimensionsScheme( + fileName, + resolvedTokenFile, + density, + device, + ); + } + } + + resolvedTokenFile += "\n}\n"; + + return resolvedTokenFile; +}; diff --git a/src/utils/outputs/swiftui/elevation.ts b/src/utils/outputs/swiftui/elevation.ts new file mode 100644 index 00000000..d8ee2db2 --- /dev/null +++ b/src/utils/outputs/swiftui/elevation.ts @@ -0,0 +1,84 @@ +import { ThemeSizing } from "../../data.ts"; + +export const generateSwiftUIElevationsFile = ( + allElevations: ThemeSizing, +): string => { + let resolvedString: string = `import SwiftUI + +struct DSShadowViewModifier: ViewModifier { + + let elevation: DSSubElevation + + func body(content: Content) -> some View { + content + .background( + content + .shadow(color: elevation.first.color, radius: elevation.first.spread, x: elevation.first.x, y: elevation.first.y) + .shadow(color: elevation.second.color, radius: elevation.second.spread, x: elevation.second.x, y: elevation.second.y) + .shadow(color: elevation.third.color, radius: elevation.third.spread, x: elevation.third.x, y: elevation.third.y) + ) + } +} + +extension View { + func dsShadow(elevation: DSSubElevation = DSElevation.sm) -> some View { + self.modifier(DSShadowViewModifier(elevation: elevation)) + } +} + +struct DSSubElevation { + let first: DSElevationShadowConfig + let second: DSElevationShadowConfig + let third: DSElevationShadowConfig +} + +struct DSElevationShadowConfig { + let x: CGFloat + let y: CGFloat + let blur: CGFloat + let spread: CGFloat + let color: Color +} + +public struct DSElevation {\n`; + + Object.entries(allElevations).forEach(([name, elevation]) => { + if (!name.includes("_scale")) { + resolvedString += ` static let ${name.toLowerCase()} = DSSubElevation(`; + const shadows = elevation.toString().replaceAll(" ", " ").replaceAll("rgba(", "").replaceAll ("), ", "#").replaceAll(")", "").replaceAll(",", "").split("#") + resolvedString += ` + first: .init( + x: ${shadows[0].split(' ')[0]}, + y: ${shadows[0].split(' ')[1]}, + blur: ${shadows[0].split(' ')[2].replace("px", "")}, + spread: ${shadows[0].split(' ')[3].replace("px", "")}, + color: Color(red: ${shadows[0].split(' ')[4]}, green: ${shadows[0].split(' ')[5]}, blue: ${shadows[0].split(' ')[6]}, opacity: ${shadows[0].split(' ')[7]}) + ), + second: .init( + x: ${shadows[1].split(' ')[0]}, + y: ${shadows[1].split(' ')[1]}, + blur: ${shadows[1].split(' ')[2].replace("px", "")}, + spread: ${shadows[1].split(' ')[3].replace("px", "")}, + color: Color(red: ${shadows[1].split(' ')[4]}, green: ${shadows[1].split(' ')[5]}, blue: ${shadows[1].split(' ')[6]}, opacity: ${shadows[1].split(' ')[7]}) + ), + third: .init( + x: ${shadows[2].split(' ')[0]}, + y: ${shadows[2].split(' ')[1]}, + blur: ${shadows[2].split(' ')[2].replace("px", "")}, + spread: ${shadows[2].split(' ')[3].replace("px", "")}, + color: Color(red: ${shadows[2].split(' ')[4]}, green: ${shadows[2].split(' ')[5]}, blue: ${shadows[2].split(' ')[6]}, opacity: ${shadows[2].split(' ')[7]}) + ) + ) +`; + } + }) + + resolvedString += "}\n"; + + resolvedString += ` + + ` + return resolvedString; +}; + + diff --git a/src/utils/outputs/swiftui/readme.ts b/src/utils/outputs/swiftui/readme.ts new file mode 100644 index 00000000..fad4d00c --- /dev/null +++ b/src/utils/outputs/swiftui/readme.ts @@ -0,0 +1,53 @@ +export const generateSwiftUIReadmeFile = (): string => { + return `# How to use the theme + +1. Move the \`theme\` directory into your project +3. Add your theme to your \`ContentView\`: + +\`\`\`\` kotlin +import SwiftUI + +@main +struct YourApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .dsTheme() + } + } +} +\`\`\`\` + +Use the tokens like this: +\`\`\`\` swift + +struct ContentView: View { + @Environment(\\.theme) var theme + + @State var scheme: DSColorVariant? + + var body: some View { + Text("Headline") + .font(theme.fonts.h1.font) + .foregroundColor(theme.activeColor.onBgBasicEmphasis80Default) + .padding(theme.dimensions.spacing.responsiveXs) + } +} +\`\`\`\` + +To use another theme, export it and copy the \`theme/\` directory to your project. Set it like this: + +\`\`\`swift +// Code from step 3 +@main +struct YourApp: App { + var body: some Scene { + WindowGroup { + ContentView() + .dsTheme(Theme()) + } + } +} +\`\`\` +`; +}; diff --git a/src/utils/outputs/swiftui/shared.ts b/src/utils/outputs/swiftui/shared.ts new file mode 100644 index 00000000..d7fc705e --- /dev/null +++ b/src/utils/outputs/swiftui/shared.ts @@ -0,0 +1,238 @@ +export const designSystemName = "DesignSystem" + +export const densities = ["Functional", "Regular", "Expressive"]; + +export const devices = ["Mobile", "Tablet"]; + +export const shirtSizes = [ + "3xs", + "2xs", + "xs", + "sm", + "md", + "lg", + "xl", + "2xl", + "3xl", +]; + +export const generateStaticSwiftUIFiles = (): string => { + return `import SwiftUI + +public struct BasicColor { + public var text: TextColor + public var icon: IconColor + public var border: BorderColor + public var background: BackgroundColor +} + +public struct InvertedBackgroundColor { + public var contrastMax: StateColor + public var contrastHigh: StateColor + public var contrastLow: StateColor +} + +public struct InvertedColor { + public var background: InvertedBackgroundColor + public var onBackground: StateColor +} + +public struct OriginColor { + public var origin: StateColor + public var onOrigin: StateColor +} + +public struct TextColor { + public var \`default\`: StateColor + public var emphasis100: StateColor + public var emphasis90: StateColor + public var emphasis80: StateColor +} + +public struct IconColor { + public var \`default\`: StateColor + public var emphasis100: StateColor + public var emphasis90: StateColor + public var emphasis80: StateColor + public var emphasis70: StateColor +} + +public struct BorderColor { + public var \`default\`: StateColor + public var emphasis100: StateColor + public var emphasis70: StateColor + public var emphasis60: StateColor + public var emphasis50: StateColor +} + +public struct BackgroundColor { + public var transparent: TransparentColor + public var level1: StateColor + public var level2: StateColor + public var level3: StateColor +} + +public struct TransparentColor { + public var full: Color + public var semi: Color + public var hovered: Color + public var pressed: Color +} + +public struct StateColor { + public var \`default\`: Color + public var hovered: Color + public var pressed: Color +} + +extension DSColorVariant { + + public var basic: BasicColor { + .init( + text: .init( + default: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis100: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis90: .init( + default: onBgBasicEmphasis80Default, + hovered: onBgBasicEmphasis80Hovered, + pressed: onBgBasicEmphasis80Pressed + ), + emphasis80: .init( + default: onBgBasicEmphasis80Default, + hovered: onBgBasicEmphasis80Hovered, + pressed: onBgBasicEmphasis80Pressed + ) + ), + icon: .init( + default: .init( + default: onBgBasicEmphasis70Default, + hovered: onBgBasicEmphasis70Hovered, + pressed: onBgBasicEmphasis70Pressed + ), + emphasis100: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis90: .init( + default: onBgBasicEmphasis90Default, + hovered: onBgBasicEmphasis90Hovered, + pressed: onBgBasicEmphasis90Pressed + ), + emphasis80: .init( + default: onBgBasicEmphasis80Default, + hovered: onBgBasicEmphasis80Hovered, + pressed: onBgBasicEmphasis80Pressed + ), + emphasis70: .init( + default: onBgBasicEmphasis70Default, + hovered: onBgBasicEmphasis70Hovered, + pressed: onBgBasicEmphasis70Pressed + ) + ), + border: .init( + default: .init( + default: onBgBasicEmphasis60Default, + hovered: onBgBasicEmphasis60Hovered, + pressed: onBgBasicEmphasis60Pressed + ), + emphasis100: .init( + default: onBgBasicEmphasis100Default, + hovered: onBgBasicEmphasis100Hovered, + pressed: onBgBasicEmphasis100Pressed + ), + emphasis70: .init( + default: onBgBasicEmphasis70Default, + hovered: onBgBasicEmphasis70Hovered, + pressed: onBgBasicEmphasis70Pressed + ), + emphasis60: .init( + default: onBgBasicEmphasis60Default, + hovered: onBgBasicEmphasis60Hovered, + pressed: onBgBasicEmphasis60Pressed + ), + emphasis50: .init( + default: onBgBasicEmphasis50Default, + hovered: onBgBasicEmphasis50Hovered, + pressed: onBgBasicEmphasis50Pressed + ) + ), + background: .init( + transparent: .init( + full: bgBasicTransparentFullDefault, + semi: bgBasicTransparentSemiDefault, + hovered: bgBasicTransparentHovered, + pressed: bgBasicTransparentPressed + ), + level1: .init( + default: bgBasicLevel1Default, + hovered: bgBasicLevel1Hovered, + pressed: bgBasicLevel1Pressed + ), + level2: .init( + default: bgBasicLevel2Default, + hovered: bgBasicLevel2Hovered, + pressed: bgBasicLevel2Pressed + ), + level3: .init( + default: bgBasicLevel3Default, + hovered: bgBasicLevel3Hovered, + pressed: bgBasicLevel3Pressed + ) + ) + ) + } + + public var inverted: InvertedColor { + .init( + background: .init( + contrastMax: .init( + default: bgInvertedContrastMaxDefault, + hovered: bgInvertedContrastMaxHovered, + pressed: bgInvertedContrastMaxPressed + ), + contrastHigh: .init( + default: bgInvertedContrastHighDefault, + hovered: bgInvertedContrastHighHovered, + pressed: bgInvertedContrastHighPressed + ), + contrastLow: .init( + default: bgInvertedContrastLowDefault, + hovered: bgInvertedContrastLowHovered, + pressed: bgInvertedContrastLowPressed + ) + ), + onBackground: .init( + default: onBgInvertedDefault, + hovered: onBgInvertedHovered, + pressed: onBgInvertedPressed + ) + ) + } + + public var origin: OriginColor { + .init( + origin: .init( + default: originDefault, + hovered: originHovered, + pressed: originPressed + ), + onOrigin: .init( + default: onOriginDefault, + hovered: onOriginHovered, + pressed: onOriginPressed + ) + ) + } +} + + ` +} \ No newline at end of file diff --git a/src/utils/outputs/swiftui/theme.ts b/src/utils/outputs/swiftui/theme.ts new file mode 100644 index 00000000..bb250935 --- /dev/null +++ b/src/utils/outputs/swiftui/theme.ts @@ -0,0 +1,117 @@ +import { designSystemName } from "./shared"; + +export const generateSwiftUIDesignSystemThemeFile = (fileName: string): string => { + console.log(fileName) + return ` +import SwiftUI + +struct ThemeModifier: ViewModifier { + @Environment(\\.colorScheme) var systemColorScheme + + var theme: DSTheme + + func body(content: Content) -> some View { + var changedTheme = theme + changedTheme.fonts = adaptiveFonts + changedTheme.dimensions = adaptiveDimensions + + return content + .theme(changedTheme) + } + + var adaptiveFonts: DesignSystemTextStyles { + // TODO: Use dimensions environment variable + let typography = UIDevice.current.userInterfaceIdiom == .pad ? DesignSystemTypography.getTypographyRegularTablet(sizes: DeutscheBahnTypography) : DesignSystemTypography.getTypographyRegularMobile(sizes: DeutscheBahnTypography) + return DesignSystemTextStyles.getFonts(typo: typography) + } + + var adaptiveDimensions: DesignSystemDimensions { + UIDevice.current.userInterfaceIdiom == .pad ? DesignSystemDimensions.getDimensionsRegularTablet(dimensions: DeutscheBahnDimensions()) : DesignSystemDimensions.getDimensionsRegularMobile(dimensions: DeutscheBahnDimensions()) + } +} + +extension EnvironmentValues { + @Entry public var theme: DSTheme = DeutscheBahnTheme() +} + +extension View { + public func dsTheme(_ theme: DSTheme = DeutscheBahnTheme()) -> some View { + modifier(ThemeModifier(theme: theme)) + } + + public func theme(_ theme: DSTheme) -> some View { + environment(\\.theme, theme) + } + + public func activeColorScheme(_ colorScheme: DSColorVariant) -> some View { + modifier(ActiveColorViewModifier(color: colorScheme)) + } + + public func dsExpressive() -> some View { + modifier(DimensionsViewModifier(dimensions: DesignSystemDimensions.getDimensionsExpressiveMobile(dimensions: DeutscheBahnDimensions()))) + } + + public func dsFunctional() -> some View { + modifier(DimensionsViewModifier(dimensions: + UIDevice.current.userInterfaceIdiom == .pad ? DesignSystemDimensions.getDimensionsFunctionalTablet(dimensions: DeutscheBahnDimensions()) : + DesignSystemDimensions.getDimensionsFunctionalMobile(dimensions: DeutscheBahnDimensions()))) + } +} + +struct ActiveColorViewModifier: ViewModifier { + @Environment(\\.theme) var theme + + var color: DSColorVariant + + func body(content: Content) -> some View { + var changedTheme = theme + changedTheme.activeColor = color + + return content + .theme(changedTheme) + } +} + +struct DimensionsViewModifier: ViewModifier { + @Environment(\\.theme) var theme + + var dimensions: DesignSystemDimensions + + func body(content: Content) -> some View { + var changedTheme = theme + changedTheme.dimensions = dimensions + + return content + .theme(changedTheme) + } +} + +public protocol DSTheme { + var colorScheme: DesignSystemColorScheme { get set } + var activeColor: DSColorVariant { get set } + var fonts: DesignSystemTextStyles { get set } + var dimensions: DesignSystemDimensions { get set } +} + +`; +} + +export const generateSwiftUIThemeFile = (themeName: string): string => { + return ` +import SwiftUI + +public struct ${themeName}Theme: DSTheme { + public var colorScheme: ${designSystemName}ColorScheme + public var activeColor: DSColorVariant + public var dimensions: ${designSystemName}Dimensions + public var fonts: ${designSystemName}TextStyles + + public init(_ colorScheme: ColorScheme = .light) { + self.colorScheme = colorScheme == .light ? DesignSystemColorScheme.getColorSchemeLight(colors: ${themeName}Colors) : DesignSystemColorScheme.getColorSchemeDark(colors: ${themeName}Colors) + self.activeColor = self.colorScheme.brand + self.dimensions = ${designSystemName}Dimensions.getDimensionsFunctionalMobile(dimensions: ${themeName}Dimensions()) + self.fonts = DesignSystemTextStyles.getFonts(typo: ${designSystemName}Typography.getTypographyFunctionalMobile(sizes: ${themeName}Typography)) + } +} +` +}; diff --git a/src/utils/outputs/swiftui/typography.ts b/src/utils/outputs/swiftui/typography.ts new file mode 100644 index 00000000..3413464c --- /dev/null +++ b/src/utils/outputs/swiftui/typography.ts @@ -0,0 +1,260 @@ +import { ThemeType } from "../../data.ts"; +import traverse from "traverse"; +import { kebabCase } from "../../index.ts"; +import { + densities, + designSystemName, + devices, + shirtSizes, +} from "./shared.ts"; + +export const generateSwiftUIFontFamilyFile = (): string => { + return `import SwiftUI +import UIKit + +struct DSFont { + let name: String + private let publicName: String + + private init(named name: String, publicName: String) { + self.name = name + self.publicName = publicName + do { + try registerFont(fontName: name) + } catch { + let reason = error.localizedDescription + fatalError("Failed to register font: \\(reason)") + } + } + + private enum FontError: Swift.Error { + case failedToRegisterFont + } + + private func registerFont(fontName: String) throws { + guard let fontURL = Bundle.module.url(forResource: "\\(fontName)", withExtension: "ttf") else { + throw FontError.failedToRegisterFont + } + + let fontURLs = [fontURL] as CFArray + + CTFontManagerRegisterFontURLs(fontURLs, .process, true) { errors, done in + let errors = errors as! [CFError] + guard errors.isEmpty else { + preconditionFailure(errors.map(\\.localizedDescription).joined()) + } + return true + } + } + + func font(size: CGFloat) -> Font { + Font.custom(publicName, size: size) + } + + func uiFont(size: CGFloat) -> UIFont { + UIFont(name: publicName, size: size)! + } + + static let dbNeoScreenFlex = DSFont(named: "DBNeoScreenFlex", publicName: "DB Neo Screen Flex") +} +`; +} + +export const generateSwiftUITypographyFile = (fileName: string, theme: ThemeType): string => { + let resolvedTokenFile: string = `import SwiftUI + +let ${fileName}Typography: [String: CGFloat] = [ +`; + + traverse(theme).forEach(function (value) { + if ( + this.isLeaf && + this.path.length === 6 && + this.path[0] === "typography" && + !this.path.includes("desktop") && + !this.path.includes("_scale") + ) { + const resolvedNameArray = [ + this.path[3], + this.path[5], + this.path[1], + this.path[2], + this.path[4], + ]; + const key = `${kebabCase(resolvedNameArray.join("-"), true)}`; + + let finalValue = `${Number(value) * 16}`; + if (this.path.at(-1) === "lineHeight") { + const fontSizePath = [...this.path]; + fontSizePath[fontSizePath.length - 1] = "fontSize"; + finalValue = `${Number(traverse(theme).get(fontSizePath)) * value * 16}`; + } + + resolvedTokenFile += ` "${key}": ${finalValue},\n`; + } + }); + + resolvedTokenFile += `]\n` + + return resolvedTokenFile; +}; + +const typoVariants: string[] = ["body", "headline"]; +const typoTypes: string[] = ["lineHeight", "fontSize"]; + +const fontsTypes: Record = { + h1: "Xl", + h2: "Lg", + h3: "Md", + h4: "Sm", + h5: "Xs", + h6: "2xs", + body: "Md", + body3xl: "3xl", + body2xl: "2xl", + bodyXl: "Xl", + bodyLg: "Lg", + bodyMd: "Md", + bodySm: "Sm", + bodyXs: "Xs", + body2xs: "2xs", + body3xs: "3xs", +}; + +export const generateSwiftUITypographyScheme = ( + fileName: string, + resolvedTokenFile: string, + density: string, + device: string, +): string => { + console.log(fileName) + + resolvedTokenFile += `extension ${designSystemName}Typography {\n` + for (const variant of typoVariants) { + resolvedTokenFile += ` private static func ${variant}Typography${density}${device}(sizes: [String: CGFloat]) -> DSTypography { .init(\n`; + resolvedTokenFile += ` variant: DSTypographyVariant.${variant.toLowerCase()},\n` + resolvedTokenFile += ` density: DSDensity.${density.toLowerCase()},\n` + resolvedTokenFile += ` device: DSDeviceType.${device.toLowerCase()},\n` + resolvedTokenFile += ` sizes: sizes\n` + resolvedTokenFile += `\n )\n }\n\n` + } + + resolvedTokenFile += ` static func getTypography${density}${device}(sizes: [String: CGFloat]) -> ${designSystemName}Typography { + .init(\n`; + for (const variant of typoVariants) { + resolvedTokenFile += ` ${variant}: ${variant}Typography${density}${device}(sizes: sizes),\n`; + } + resolvedTokenFile = resolvedTokenFile.substring(0, resolvedTokenFile.lastIndexOf(',')); + resolvedTokenFile += `\n )\n }\n}\n\n`; + + return resolvedTokenFile; +}; + +export const generateSwiftUITypographySchemeFile = (fileName: string): string => { + let resolvedTokenFile: string = `import SwiftUI\n\n`; + + resolvedTokenFile += `struct DSTypography {\n`; + for (const type of typoTypes) { + for (const size of shirtSizes) { + resolvedTokenFile += ` let ${kebabCase(`${type}-${size}`, true)}: CGFloat\n`; + } + } + resolvedTokenFile += "}\n\n"; + + resolvedTokenFile += `struct ${designSystemName}Typography {\n`; + for (const variant of typoVariants) { + resolvedTokenFile += ` let ${variant}: DSTypography\n`; + } + resolvedTokenFile += "}\n\n"; + + for (const density of densities) { + for (const device of devices) { + resolvedTokenFile = generateSwiftUITypographyScheme( + fileName, + resolvedTokenFile, + density, + device, + ); + } + } + + resolvedTokenFile += ` +enum DSDeviceType: String { + case mobile = "Mobile" + case tablet = "Tablet" +} + +enum DSTypographyVariant: String { + case body + case headline +} + +extension DSTypography { + init(variant: DSTypographyVariant, density: DSDensity, device: DSDeviceType, sizes: [String: CGFloat]) { + lineHeight3xs = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)3xs", default: 12] + lineHeight2xs = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)2xs", default: 12] + lineHeightXs = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)Xs", default: 12] + lineHeightSm = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)Sm", default: 12] + lineHeightMd = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)Md", default: 12] + lineHeightLg = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)Lg", default: 12] + lineHeightXl = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)Xl", default: 12] + lineHeight2xl = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)2xl", default: 12] + lineHeight3xl = sizes["\\(variant)LineHeight\\(density.rawValue)\\(device.rawValue)3xl", default: 12] + + fontSize3xs = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)3xs", default: 12] + fontSize2xs = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)2xs", default: 12] + fontSizeXs = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)2xs", default: 12] + fontSizeSm = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)Sm", default: 12] + fontSizeMd = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)Md", default: 12] + fontSizeLg = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)Lg", default: 12] + fontSizeXl = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)Xl", default: 12] + fontSize2xl = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)2xl", default: 12] + fontSize3xl = sizes["\\(variant)FontSize\\(density.rawValue)\\(device.rawValue)3xl", default: 12] + } +} + +public struct DSTextStyle { + public let font: Font + let uiFont: UIFont + public let lineHeight: CGFloat + public let fontWeight: Font.Weight +} + +` + + resolvedTokenFile += `public struct ${designSystemName}TextStyles{\n`; + for (const [font] of Object.entries(fontsTypes)) { + resolvedTokenFile += ` public let ${font}: DSTextStyle\n`; + } + resolvedTokenFile += `\n\n`; + + resolvedTokenFile += ` static func getFonts(typo: ${designSystemName}Typography) -> ${designSystemName}TextStyles { + .init(\n`; + for (const [font, size] of Object.entries(fontsTypes)) { + resolvedTokenFile += ` ${font}: .init(font: DSFont.dbNeoScreenFlex.font(size: typo.${font.includes("body") ? "body" : "headline"}.fontSize${size}), uiFont: DSFont.dbNeoScreenFlex.uiFont(size: typo.${font.includes("body") ? "body" : "headline"}.fontSize${size}), lineHeight: typo.${font.includes("body") ? "body" : "headline"}.lineHeight${size}, fontWeight: ${font.includes("body") ? ".regular" : ".black"}),\n` + } + resolvedTokenFile = resolvedTokenFile.substring(0, resolvedTokenFile.lastIndexOf(',')); + resolvedTokenFile += `\n )\n }\n}\n`; + + resolvedTokenFile += ` +struct TextStyleViewModifier: ViewModifier { + let font: DSTextStyle + + func body(content: Content) -> some View { + content + .font(font.font) + .fontWeight(font.fontWeight) + .lineSpacing(font.lineHeight - font.uiFont.lineHeight) + .padding(.vertical, (font.lineHeight - font.uiFont.lineHeight) / 2) + } +} + +extension View { + public func dsTextStyle(_ font: DSTextStyle) -> some View { + modifier(TextStyleViewModifier(font: font)) + } +} +`; + + return resolvedTokenFile; +};