Quellcode durchsuchen

Bugfix: Theme merge problems fixed.

lfabl vor 2 Wochen
Ursprung
Commit
4bec683515

+ 113 - 0
eslint-local-rules/index.js

@@ -849,5 +849,118 @@ module.exports = {
                 }
             };
         }
+    },
+    "jsx-ternary-formatting": {
+        meta: {
+            type: "layout",
+            fixable: "whitespace",
+            schema: []
+        },
+        create(context) {
+            return {
+                JSXExpressionContainer(node) {
+                    if (node.expression.type !== "ConditionalExpression") return;
+
+                    const sourceCode = context.getSourceCode();
+                    const openBrace = sourceCode.getFirstToken(node);
+                    const closeBrace = sourceCode.getLastToken(node);
+                    const cond = node.expression;
+
+                    let hasJSX = false;
+                    if (
+                        cond.consequent.type === "JSXElement" ||
+                        cond.consequent.type === "JSXFragment" ||
+                        cond.alternate.type === "JSXElement" ||
+                        cond.alternate.type === "JSXFragment"
+                    ) {
+                        hasJSX = true;
+                    }
+
+                    if (!hasJSX) return;
+
+                    const checkAndRemoveParens = (nodeToUnparen) => {
+                        const currentToken = sourceCode.getTokenBefore(nodeToUnparen);
+                        const afterToken = sourceCode.getTokenAfter(nodeToUnparen);
+                        let removed = false;
+                        while (currentToken && currentToken.value === "(" && afterToken && afterToken.value === ")") {
+                            context.report({
+                                node: currentToken,
+                                message: "Parentheses are not allowed around JSX in ternaries.",
+                                fix(fixer) {
+                                    return [
+                                        fixer.remove(currentToken),
+                                        fixer.remove(afterToken)
+                                    ];
+                                }
+                            });
+                            removed = true;
+
+                            break;
+                        }
+                        return removed;
+                    };
+
+                    let hasParens = false;
+                    if (cond.consequent.type === "JSXElement" || cond.consequent.type === "JSXFragment") {
+                        if (checkAndRemoveParens(cond.consequent)) hasParens = true;
+                    }
+                    if (cond.alternate.type === "JSXElement" || cond.alternate.type === "JSXFragment") {
+                        if (checkAndRemoveParens(cond.alternate)) hasParens = true;
+                    }
+
+                    if (hasParens) return; // Wait for the next fix pass to format the whitespace properly
+
+                    const questionToken = sourceCode.getTokenAfter(cond.test, {
+                        filter: t => t.value === "?"
+                    });
+                    const colonToken = sourceCode.getTokenAfter(cond.consequent, {
+                        filter: t => t.value === ":"
+                    });
+
+                    const getIndent = (token) => {
+                        const line = sourceCode.lines[token.loc.start.line - 1];
+                        return line.match(/^\s*/)[0];
+                    };
+
+                    const openIndent = getIndent(openBrace);
+                    const baseIndent = openIndent + "    ";
+                    const innerIndent = baseIndent + "    ";
+
+                    const checkAndFix = (token1, token2, expectedWhitespace) => {
+                        let textBetween = sourceCode.getText().slice(token1.range[1], token2.range[0]);
+
+                        textBetween = textBetween.replace(/\r\n/g, "\n");
+                        if (textBetween !== expectedWhitespace) {
+                            context.report({
+                                node: token2,
+                                message: "Incorrect ternary formatting",
+                                fix(fixer) {
+                                    return fixer.replaceTextRange([
+                                        token1.range[1],
+                                        token2.range[0]
+                                    ], expectedWhitespace);
+                                }
+                            });
+                        }
+                    };
+
+                    const firstTestToken = sourceCode.getTokenAfter(openBrace);
+                    checkAndFix(openBrace, firstTestToken, "\n" + baseIndent);
+
+                    const firstConsequentToken = sourceCode.getTokenAfter(questionToken);
+                    checkAndFix(questionToken, firstConsequentToken, "\n" + innerIndent);
+
+                    const firstColonToken = colonToken;
+                    const lastConsequentToken = sourceCode.getTokenBefore(colonToken);
+                    checkAndFix(lastConsequentToken, firstColonToken, "\n" + baseIndent);
+
+                    const firstAlternateToken = sourceCode.getTokenAfter(colonToken);
+                    checkAndFix(colonToken, firstAlternateToken, "\n" + innerIndent);
+
+                    const lastAlternateToken = sourceCode.getTokenBefore(closeBrace);
+                    checkAndFix(lastAlternateToken, closeBrace, "\n" + openIndent);
+                }
+            };
+        }
     }
 };

+ 4 - 0
eslint.config.mjs

@@ -24,6 +24,7 @@ export default tseslint.config(
     {
         rules: {
             "local-rules/multiline-import-specifiers": "error",
+            "local-rules/jsx-ternary-formatting": "error",
             "local-rules/multiline-object-properties": "error",
             "local-rules/multiline-jsx-attributes": "error",
             "local-rules/multiline-array-elements": "error",
@@ -115,6 +116,9 @@ export default tseslint.config(
                 "error",
                 4,
                 {
+                    "ignoredNodes": [
+                        "ConditionalExpression"
+                    ],
                     "SwitchCase": 1
                 }
             ],

+ 48 - 36
example/src/navigation/index.tsx

@@ -33,55 +33,56 @@ import {
     GoalIcon,
     HomeIcon
 } from "lucide-react-native";
-import NotificationIndicatorPage from "../pages/components/notificationIndicator";
-import HighlightButtonPage from "../pages/components/highlightButton";
-import PaletteSwitcherPage from "../pages/components/paletteSwitcher";
-import LocaleExamplesPage from "../pages/coreFeatures/localeExamples";
+import AvatarPage from "../pages/components/avatar";
+import AvatarGroupPage from "../pages/components/avatarGroup";
+import BottomSheetPage from "../pages/components/bottomSheet";
+import ButtonPage from "../pages/components/button";
+import CheckBoxPage from "../pages/components/checkBox";
+import ChipPage from "../pages/components/chip";
+import CodeViewerPage from "../pages/components/codeViewer";
+import DateSelectorPage from "../pages/components/dateSelector";
 import DateTimePickerPage from "../pages/components/dateTimePicker";
+import DateTimeSheetPage from "../pages/components/dateTimeSheet";
+import DialogPage from "../pages/components/dialog";
+import EmbeddedMenuPage from "../pages/components/embeddedMenu";
+import HeaderPage from "../pages/components/header";
+import HighlightButtonPage from "../pages/components/highlightButton";
+import LoadingPage from "../pages/components/loading";
 import LocaleSwitcherPage from "../pages/components/localeSwitcher";
+import MainHeaderPage from "../pages/components/mainHeader";
 import MarkdownViewerPage from "../pages/components/markdownViewer";
-import ThemeExamplesPage from "../pages/coreFeatures/themeExamples";
-import DateTimeSheetPage from "../pages/components/dateTimeSheet";
+import MenuPage from "../pages/components/menu";
+import ModalPage from "../pages/components/modal";
 import MonthSelectorPage from "../pages/components/monthSelector";
-import PageContainerPage from "../pages/components/pageContainer";
-import TextAreaInputPage from "../pages/components/textAreaInput";
-import ThemeSwitcherPage from "../pages/components/themeSwitcher";
-import DateSelectorPage from "../pages/components/dateSelector";
-import EmbeddedMenuPage from "../pages/components/embeddedMenu";
+import NotificationIndicatorPage from "../pages/components/notificationIndicator";
 import NumericInputPage from "../pages/components/numericInput";
-import TimeSelectorPage from "../pages/components/timeSelector";
-import WebScrollbarPage from "../pages/components/webScrollbar";
-import YearSelectorPage from "../pages/components/yearSelector";
-import AvatarGroupPage from "../pages/components/avatarGroup";
-import BottomSheetPage from "../pages/components/bottomSheet";
+import PageContainerPage from "../pages/components/pageContainer";
+import PaletteSwitcherPage from "../pages/components/paletteSwitcher";
 import RadioButtonPage from "../pages/components/radioButton";
-import SelectSheetPage from "../pages/components/selectSheet";
-import ToolsUsagePage from "../pages/coreFeatures/toolsUsage";
-import CodeViewerPage from "../pages/components/codeViewer";
-import MainHeaderPage from "../pages/components/mainHeader";
+import RowCardPage from "../pages/components/rowCard";
 import SelectBoxPage from "../pages/components/selectBox";
+import SelectSheetPage from "../pages/components/selectSheet";
 import SeperatorPage from "../pages/components/seperator";
-import StateCardPage from "../pages/components/stateCard";
-import TextInputPage from "../pages/components/textInput";
-import ImplementationPage from "../pages/implementation";
-import CheckBoxPage from "../pages/components/checkBox";
 import SiteLogoPage from "../pages/components/siteLogo";
 import SnackBarPage from "../pages/components/snackBar";
-import LoadingPage from "../pages/components/loading";
-import RowCardPage from "../pages/components/rowCard";
+import StateCardPage from "../pages/components/stateCard";
 import StickerPage from "../pages/components/sticker";
-import InstallationPage from "../pages/installation";
-import AvatarPage from "../pages/components/avatar";
-import ButtonPage from "../pages/components/button";
-import DialogPage from "../pages/components/dialog";
-import HeaderPage from "../pages/components/header";
 import SwitchPage from "../pages/components/switch";
-import ModalPage from "../pages/components/modal";
-import ToastPage from "../pages/components/toast";
-import ChipPage from "../pages/components/chip";
-import MenuPage from "../pages/components/menu";
 import TextPage from "../pages/components/text";
+import TextAreaInputPage from "../pages/components/textAreaInput";
+import TextInputPage from "../pages/components/textInput";
+import ThemeSwitcherPage from "../pages/components/themeSwitcher";
+import TimeSelectorPage from "../pages/components/timeSelector";
+import ToastPage from "../pages/components/toast";
+import WebScrollbarPage from "../pages/components/webScrollbar";
+import YearSelectorPage from "../pages/components/yearSelector";
+import DefaultJSONsPage from "../pages/coreFeatures/defaultJSONs";
+import LocaleExamplesPage from "../pages/coreFeatures/localeExamples";
+import ThemeExamplesPage from "../pages/coreFeatures/themeExamples";
+import ToolsUsagePage from "../pages/coreFeatures/toolsUsage";
 import Home from "../pages/home";
+import ImplementationPage from "../pages/implementation";
+import InstallationPage from "../pages/installation";
 
 const RootStack = createNativeStackNavigator();
 const ComponentsStack = createNativeStackNavigator();
@@ -111,6 +112,10 @@ const CoreFeaturesNav = () => {
             component={TypesOverridePage}
             name="TypesOverride"
         />
+        <CoreFeaturesStack.Screen
+            component={DefaultJSONsPage}
+            name="DefaultJSONs"
+        />
     </CoreFeaturesStack.Navigator>;
 };
 
@@ -462,6 +467,12 @@ const RootNav = () => {
                             redirectSub: "TypesOverride",
                             title: "typesOverride",
                             isUseLocalize: true
+                        },
+                        {
+                            redirectMain: "CoreFeatures",
+                            redirectSub: "DefaultJSONs",
+                            title: "defaultJSONs",
+                            isUseLocalize: true
                         }
                     ]
                 },
@@ -826,7 +837,8 @@ const Navigation = () => {
                             ToolsUsage: "toolsusage",
                             ThemeExamples: "themeexamples",
                             LocaleExamples: "localeexamples",
-                            TypesOverride: "typesoverride"
+                            TypesOverride: "typesoverride",
+                            DefaultJSONs: "defaultjsons"
                         }
                     },
                     Components: {

+ 1 - 0
example/src/navigation/type.ts

@@ -11,6 +11,7 @@ export type CoreFeaturesStackParamList = {
     LocaleExamples: undefined;
     ThemeExamples: undefined;
     TypesOverride: undefined;
+    DefaultJSONs: undefined;
     ToolsUsage: undefined;
 };
 

+ 200 - 0
example/src/pages/coreFeatures/defaultJSONs/index.tsx

@@ -0,0 +1,200 @@
+import {
+    useLayoutEffect
+} from "react";
+import {
+    View
+} from "react-native";
+import stylesheet from "./stylesheet";
+import {
+    NCoreUIKitLocalize,
+    NCoreUIKitTheme,
+    PageContainer,
+    CodeViewer,
+    Button,
+    Header,
+    Text
+} from "ncore-ui-kit";
+import {
+    useNavigation
+} from "@react-navigation/native";
+import type RootStackParamList from "../../../navigation/type";
+import type {
+    NativeStackNavigationProp
+} from "@react-navigation/native-stack";
+import {
+    ChevronRightIcon,
+    ChevronLeftIcon
+} from "lucide-react-native";
+import defaultLocaleJSON from "../../../../../src/variants/locales/default.json";
+import defaultThemeJSON from "../../../../../src/variants/themes/default.json";
+
+const DefaultJSONs = () => {
+    const {
+        colors,
+        spaces
+    } = NCoreUIKitTheme.useContext();
+
+    const {
+        localize
+    } = NCoreUIKitLocalize.useContext();
+
+    const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
+
+    useLayoutEffect(() => {
+        navigation.setOptions({
+            header: () => <Header
+                title={localize("defaultJSONs-title")}
+                isWrapSafeareaContext={false}
+                navigation={navigation as unknown as NCoreUIKit.Navigation}
+                isGoBackEnable={true}
+            />,
+            headerShown: true
+        });
+    }, [
+        navigation,
+        localize
+    ]);
+
+    const themeCode = JSON.stringify(
+        {
+            ...defaultThemeJSON,
+            palettes: defaultThemeJSON.palettes.filter((palette) => palette.name === "nibgat")
+        },
+        null,
+        4
+    );
+
+    const localeCode = JSON.stringify(
+        defaultLocaleJSON.map((localeObj) => {
+            return {
+                ...localeObj,
+                translations: Object.fromEntries(Object.entries(localeObj.translations).slice(0, 3))
+            };
+        }),
+        null,
+        4
+    );
+
+    return <PageContainer
+        isWorkWithHeaderSpace={false}
+        isScrollable={true}
+    >
+        <Text
+            variant="headlineSmallSize"
+            color="high"
+        >
+            {localize("defaultJSONs-title")}
+        </Text>
+        <Text
+            color="mid"
+            style={{
+                marginTop: spaces.spacingXs
+            }}
+        >
+            {localize("defaultJSONs-desc")}
+        </Text>
+
+        <Text
+            variant="titleMediumSize"
+            color="high"
+            style={{
+                marginTop: spaces.spacingXl
+            }}
+        >
+            defaultLocaleJSON
+        </Text>
+        <Text
+            color="mid"
+            style={{
+                marginTop: spaces.spacingXs
+            }}
+        >
+            {localize("defaultJSONs-localeDesc")}
+        </Text>
+        <View
+            style={{
+                marginTop: spaces.spacingLg
+            }}
+        >
+            <CodeViewer
+                code={localeCode}
+                language="json"
+            />
+        </View>
+
+        <Text
+            variant="titleMediumSize"
+            color="high"
+            style={{
+                marginTop: spaces.spacingXl
+            }}
+        >
+            defaultThemeJSON
+        </Text>
+        <Text
+            color="mid"
+            style={{
+                marginTop: spaces.spacingXs
+            }}
+        >
+            {localize("defaultJSONs-themeDesc")}
+        </Text>
+        <View
+            style={{
+                marginTop: spaces.spacingLg
+            }}
+        >
+            <CodeViewer
+                code={themeCode}
+                autoFold={true}
+                language="json"
+            />
+        </View>
+
+        <View
+            style={[
+                stylesheet.rowSpaceBetween,
+                {
+                    marginTop: spaces.spacingXl
+                }
+            ]}
+        >
+            <Button
+                icon={({
+                    color,
+                    size
+                }) => {
+                    return <ChevronLeftIcon
+                        color={colors.content.icon[color]}
+                        size={size + 3}
+                    />;
+                }}
+                title={localize("typesOverride")}
+                onPress={() => {
+                    navigation.navigate("CoreFeatures", {
+                        screen: "TypesOverride"
+                    });
+                }}
+            />
+            <Button
+                icon={({
+                    color,
+                    size
+                }) => {
+                    return <ChevronRightIcon
+                        color={colors.content.icon[color]}
+                        size={size + 3}
+                    />;
+                }}
+                title={localize("components")}
+                iconDirection="right"
+                onPress={() => {
+                    navigation.navigate("Components", {
+                        screen: "Text"
+                    });
+                }}
+            />
+        </View>
+    </PageContainer>;
+};
+export default DefaultJSONs;

+ 12 - 0
example/src/pages/coreFeatures/defaultJSONs/stylesheet.ts

@@ -0,0 +1,12 @@
+import {
+    StyleSheet
+} from "react-native";
+
+const stylesheet = StyleSheet.create({
+    rowSpaceBetween: {
+        justifyContent: "space-between",
+        flexDirection: "row",
+        alignItems: "center"
+    }
+});
+export default stylesheet;

+ 3 - 3
example/src/pages/coreFeatures/typesOverride/index.tsx

@@ -211,11 +211,11 @@ const TypesOverride = () => {
                         size={size + 3}
                     />;
                 }}
-                title={localize("components")}
+                title={localize("defaultJSONs")}
                 iconDirection="right"
                 onPress={() => {
-                    navigation.navigate("Components", {
-                        screen: "Text"
+                    navigation.navigate("CoreFeatures", {
+                        screen: "DefaultJSONs"
                     });
                 }}
             />

+ 4 - 6
example/src/pages/implementation/index.tsx

@@ -22,8 +22,8 @@ import type {
     NativeStackNavigationProp
 } from "@react-navigation/native-stack";
 import {
-    ChevronLeftIcon,
-    ChevronRightIcon
+    ChevronRightIcon,
+    ChevronLeftIcon
 } from "lucide-react-native";
 
 const Implementation = () => {
@@ -102,15 +102,14 @@ export default App;`;
             >
                 {localize("implementation-desc")}
             </Text>
-
             <View
                 style={[
                     stylesheet.borderedCard,
                     {
                         backgroundColor: colors.content.container.default,
                         borderColor: colors.content.border.subtle,
-                        padding: spaces.spacingMd,
-                        marginBottom: spaces.spacingMd
+                        marginBottom: spaces.spacingMd,
+                        padding: spaces.spacingMd
                     }
                 ]}
             >
@@ -124,7 +123,6 @@ export default App;`;
                     language="tsx"
                 />
             </View>
-
             <View
                 style={[
                     stylesheet.rowSpaceBetween,

+ 6 - 7
example/src/pages/installation/index.tsx

@@ -70,15 +70,14 @@ const Installation = () => {
             >
                 {localize("installation-desc")}
             </Text>
-
             <View
                 style={[
                     stylesheet.borderedCard,
                     {
                         backgroundColor: colors.content.container.default,
                         borderColor: colors.content.border.subtle,
-                        padding: spaces.spacingMd,
-                        marginBottom: spaces.spacingMd
+                        marginBottom: spaces.spacingMd,
+                        padding: spaces.spacingMd
                     }
                 ]}
             >
@@ -89,18 +88,18 @@ const Installation = () => {
                 </Text>
                 <CodeViewer
                     code={"yarn add ncore-ui-kit"}
+                    isLineNumberVisible={false}
                     language="bash"
                 />
             </View>
-
             <View
                 style={[
                     stylesheet.borderedCard2,
                     {
                         backgroundColor: colors.content.container.default,
                         borderColor: colors.content.border.subtle,
-                        padding: spaces.spacingMd,
-                        marginBottom: spaces.spacingMd
+                        marginBottom: spaces.spacingMd,
+                        padding: spaces.spacingMd
                     }
                 ]}
             >
@@ -118,10 +117,10 @@ const Installation = () => {
                 </Text>
                 <CodeViewer
                     code="yarn add @react-native-clipboard/clipboard lucide-react-native moment@https://git.nibgat.space/nibgat-community/moment.git ncore-context react-native-safe-area-context react-native-svg rrule@https://git.nibgat.space/nibgat-community/rrule.git"
+                    isLineNumberVisible={false}
                     language="bash"
                 />
             </View>
-
             <View
                 style={[
                     stylesheet.rowEnd,

+ 12 - 2
example/src/variants/locales/default.json

@@ -367,7 +367,12 @@
             "close": "Kapat",
             "right": "Sağ",
             "chip": "Çip",
-            "left": "Sol"
+            "left": "Sol",
+            "defaultJSONs-title": "Varsayılan JSON Dosyaları",
+            "defaultJSONs-desc": "Projenizde NCore UI Kit'i başlatırken kendi temanızı ve dil dosyanızı nasıl tanımlayacağınızı gösteren örnek yapı taşları.",
+            "defaultJSONs-themeDesc": "Uygulamanızın renk paletlerini, tipografisini ve boşluk değerlerini belirleyen tema konfigürasyonu örneği.",
+            "defaultJSONs-localeDesc": "Uygulamanızın desteklediği dilleri ve çevirileri barındıran yerelleştirme konfigürasyonu örneği.",
+            "defaultJSONs": "Varsayılan JSONlar"
         },
         "locale": "tr-TR",
         "isRTL": false
@@ -740,7 +745,12 @@
             "chip": "Chip",
             "left": "Left",
             "next": "Next",
-            "text": "Text"
+            "text": "Text",
+            "defaultJSONs-title": "Default JSON Files",
+            "defaultJSONs-desc": "Example building blocks showing how to define your own theme and language file when initializing NCore UI Kit in your project.",
+            "defaultJSONs-themeDesc": "Example of a theme configuration that determines your application's color palettes, typography, and spacing values.",
+            "defaultJSONs-localeDesc": "Example of a localization configuration that holds the languages and translations supported by your application.",
+            "defaultJSONs": "Default JSONs"
         },
         "locale": "en-US",
         "isRTL": false

+ 284 - 76
src/components/codeViewer/index.tsx

@@ -6,6 +6,7 @@ import {
 } from "react";
 import {
     Text as NativeText,
+    TouchableOpacity,
     ScrollView,
     Platform,
     View
@@ -18,6 +19,8 @@ import stylesheet, {
     useStyles
 } from "./stylesheet";
 import {
+    splitTokensIntoLines,
+    getLineIndentation,
     parseCode
 } from "./parser";
 import {
@@ -27,14 +30,21 @@ import {
 } from "../../core/hooks";
 import Clipboard from "@react-native-clipboard/clipboard";
 import {
+    ChevronRightIcon,
+    ChevronDownIcon,
     CheckIcon,
     CopyIcon
 } from "lucide-react-native";
-
+import {
+    webStyle
+} from "../../utils";
 import Button from "../button";
-import Text from "../text";
 
 const CodeViewer = ({
+    isLineNumberVisible = true,
+    autoFoldMaxLines = 60,
+    isCollapsible = true,
+    autoFold = false,
     language,
     code
 }: ICodeViewerProps) => {
@@ -55,77 +65,177 @@ const CodeViewer = ({
 
     const timeoutRef = useRef<NodeJS.Timeout | null>(null);
 
-    const containerRef = useRef<import("react").ComponentRef<typeof View>>(null);
 
-    const [
-        stickyTop,
-        setStickyTop
-    ] = useState<number>(0);
 
     useEffect(() => {
-        if (Platform.OS !== "web") {
-            return;
-        }
-
-        const handleScroll = (e: Event) => {
-            const el = containerRef.current as unknown as HTMLElement;
-
-            if (!el || !el.getBoundingClientRect) {
-                return;
+        return () => {
+            if (timeoutRef.current) {
+                clearTimeout(timeoutRef.current);
             }
+        };
+    }, []);
 
-            const scrollContainer = e.target as Document | HTMLElement;
+    const tokens = useMemo(() => parseCode(code, language), [
+        language,
+        code
+    ]);
 
-            if (scrollContainer !== document && "contains" in scrollContainer && !scrollContainer.contains(el)) {
-                return;
-            }
+    const lines = useMemo(() => {
+        return splitTokensIntoLines(tokens);
+    }, [tokens]);
 
-            const rect = el.getBoundingClientRect();
-            let parentTop = 0;
+    const lineIndentations = useMemo(() => {
+        return lines.map((line) => getLineIndentation(line));
+    }, [lines]);
 
-            if (scrollContainer !== document && "getBoundingClientRect" in scrollContainer) {
-                parentTop = scrollContainer.getBoundingClientRect().top;
-            }
+    const isLineFoldable = useMemo(() => {
+        return lines.map((_, i) => {
+            const currentIndent = lineIndentations[i] as number;
+            for (let j = i + 1; j < lines.length; j++) {
+                const nextIndent = lineIndentations[j] as number;
+                const nextLineStr = (lines[j] as import("./type").Token[]).map((t) => t.value).join("").trim();
 
-            const offset = parentTop - rect.top;
-            const maxOffset = rect.height - (50 + (spaces.spacingMd * 2));
+                if (nextLineStr.length === 0) continue;
 
-            if (offset > 0) {
-                setStickyTop(Math.min(offset, maxOffset));
-            } else {
-                setStickyTop(0);
+                if (nextIndent > currentIndent) {
+                    return true;
+                } else {
+                    return false;
+                }
             }
-        };
-
-        window.addEventListener("scroll", handleScroll, true);
+            return false;
+        });
+    }, [
+        lines,
+        lineIndentations
+    ]);
 
-        return () => {
-            window.removeEventListener("scroll", handleScroll, true);
-        };
-    }, []);
+    const hasAnyFoldableLine = useMemo(() => {
+        return isLineFoldable.some((foldable) => foldable);
+    }, [isLineFoldable]);
 
-    useEffect(() => {
-        return () => {
-            if (timeoutRef.current) {
-                clearTimeout(timeoutRef.current);
-            }
-        };
-    }, []);
+    const actualIsCollapsible = isCollapsible && hasAnyFoldableLine;
 
     const {
+        languageText: languageTextDynamicStyle,
         container: containerDynamicStyle,
+        lineRow: lineRowDynamicStyle,
+        gutter: gutterDynamicStyle,
         text: textDynamicStyle
     } = useStyles({
+        isLineNumberVisible,
+        isCollapsible: actualIsCollapsible,
         typography,
         colors,
         spaces
     });
 
-    const tokens = useMemo(() => parseCode(code, language), [
-        language,
-        code
+    const initialCollapsedLines = useMemo(() => {
+        const collapsed = new Set<number>();
+        if (!autoFold || !actualIsCollapsible) return collapsed;
+
+        let minIndent = Infinity;
+        for (let i = 0; i < lines.length; i++) {
+            if (isLineFoldable[i]) {
+                const indent = lineIndentations[i] as number;
+                if (indent < minIndent) {
+                    minIndent = indent;
+                }
+            }
+        }
+
+        for (let i = 0; i < lines.length; i++) {
+            if (isLineFoldable[i]) {
+                const startIndent = lineIndentations[i] as number;
+
+                if (startIndent <= minIndent) {
+                    continue;
+                }
+
+                let blockLength = 0;
+
+                for (let j = i + 1; j < lines.length; j++) {
+                    const currentIndent = lineIndentations[j] as number;
+                    const lineStr = (lines[j] as import("./type").Token[]).map((t) => t.value).join("").trim();
+
+                    if (lineStr.length !== 0 && currentIndent <= startIndent) {
+                        break;
+                    }
+
+                    blockLength++;
+                }
+
+                if (blockLength > autoFoldMaxLines) {
+                    collapsed.add(i);
+                }
+            }
+        }
+        return collapsed;
+    }, [
+        autoFoldMaxLines,
+        lineIndentations,
+        isLineFoldable,
+        autoFold,
+        lines
+    ]);
+
+    const [
+        collapsedLines,
+        setCollapsedLines
+    ] = useState<Set<number>>(initialCollapsedLines);
+
+    useEffect(() => {
+        setCollapsedLines(initialCollapsedLines);
+    }, [
+        initialCollapsedLines
     ]);
 
+    const visibleLines = useMemo(() => {
+        const visible = new Set<number>();
+        const activeFolds: number[] = [];
+
+        for (let i = 0; i < lines.length; i++) {
+            const indent = lineIndentations[i] as number;
+            const lineStr = (lines[i] as import("./type").Token[]).map((t) => t.value).join("").trim();
+            const isEmpty = lineStr.length === 0;
+
+            if (!isEmpty) {
+                while (activeFolds.length > 0 && indent <= (activeFolds[activeFolds.length - 1] as number)) {
+                    activeFolds.pop();
+                }
+            }
+
+            if (activeFolds.length > 0) {
+                continue;
+            }
+
+            visible.add(i);
+
+            if (actualIsCollapsible && collapsedLines.has(i) && isLineFoldable[i]) {
+                activeFolds.push(indent);
+            }
+        }
+        return visible;
+    }, [
+        lineIndentations,
+        isLineFoldable,
+        collapsedLines,
+        actualIsCollapsible,
+        lines
+    ]);
+
+    const toggleFold = (lineIndex: number) => {
+        setCollapsedLines((prev) => {
+            const next = new Set(prev);
+            if (next.has(lineIndex)) {
+                next.delete(lineIndex);
+            } else {
+                next.add(lineIndex);
+            }
+            return next;
+        });
+    };
+
     const getTokenColor = (type: TokenType) => {
         return colors.code[type as keyof typeof colors.code] || colors.code.default;
     };
@@ -176,7 +286,6 @@ const CodeViewer = ({
     };
 
     return <View
-        ref={containerRef}
         style={[
             stylesheet.container,
             containerDynamicStyle
@@ -193,40 +302,139 @@ const CodeViewer = ({
                 }
             ]}
         >
-            <Text
-                style={[
-                    stylesheet.content,
-                    textDynamicStyle
-                ]}
-            >
-                {language ? <Text
-                    color="low"
-                >
-                    // {language}{"\n\n"}
-                </Text> : null}
-                {tokens.map((token, index) => {
-                    return <NativeText
-                        key={`code-token-${index}`}
-                        style={{
-                            fontFamily: Platform.OS === "web" ? "monospace" : "System",
-                            color: getTokenColor(token.type)
-                        }}
+            <View>
+                {
+                    language ?
+                        <NativeText
+                            selectable={false}
+                            style={[
+                                stylesheet.content,
+                                textDynamicStyle,
+                                languageTextDynamicStyle
+                            ]}
+                        >
+                            // {language}
+                        </NativeText>
+                    :
+                        null
+                }
+                {lines.map((lineTokens, index) => {
+                    if (!visibleLines.has(index)) return null;
+
+                    const isFoldable = isLineFoldable[index];
+                    const isCollapsed = collapsedLines.has(index);
+                    const isGutterVisible = isLineNumberVisible || actualIsCollapsible;
+
+                    return <View
+                        key={`line-${index}`}
+                        style={[
+                            stylesheet.lineRow,
+                            lineRowDynamicStyle
+                        ]}
                     >
-                        {token.value}
-                    </NativeText>;
+                        {
+                            isGutterVisible ?
+                                <View
+                                    style={[
+                                        stylesheet.gutter,
+                                        gutterDynamicStyle,
+                                        {
+                                            borderRightColor: colors.content.border.subtle
+                                        }
+                                    ]}
+                                >
+                                    {
+                                        isLineNumberVisible ?
+                                            <NativeText
+                                                selectable={false}
+                                                style={[
+                                                    stylesheet.lineNumber,
+                                                    {
+                                                        fontFamily: Platform.OS === "web" ? "monospace" : "System",
+                                                        color: colors.content.text.low
+                                                    }
+                                                ]}
+                                            >
+                                                {index + 1}
+                                            </NativeText>
+                                        :
+                                            null
+                                    }
+                                    {
+                                        actualIsCollapsible ?
+                                            isFoldable ?
+                                                <TouchableOpacity
+                                                    style={stylesheet.foldIcon}
+                                                    onPress={() => {
+                                                        toggleFold(index);
+                                                    }}
+                                                >
+                                                    {
+                                                        isCollapsed ?
+                                                            <ChevronRightIcon
+                                                                color={colors.content.text.low}
+                                                                size={14}
+                                                            />
+                                                        :
+                                                            <ChevronDownIcon
+                                                                color={colors.content.text.low}
+                                                                size={14}
+                                                            />
+                                                    }
+                                                </TouchableOpacity>
+                                                :
+                                                <View
+                                                    style={stylesheet.foldIconPlaceholder}
+                                                />
+                                            :
+                                            null
+                                    }
+                                </View>
+                            :
+                                null
+                        }
+                        <NativeText
+                            style={[
+                                stylesheet.content,
+                                textDynamicStyle,
+                                {
+                                    color: colors.content.text.high
+                                }
+                            ]}
+                        >
+                            {lineTokens.map((token, tIndex) => {
+                                return <NativeText
+                                    key={`token-${index}-${tIndex}`}
+                                    style={{
+                                        color: getTokenColor(token.type)
+                                    }}
+                                >
+                                    {token.value}
+                                </NativeText>;
+                            })}
+                            {isCollapsed && (
+                                <NativeText
+                                    style={{
+                                        backgroundColor: colors.content.container.subtle,
+                                        color: colors.content.text.mid
+                                    }}
+                                >
+                                    {" ... "}
+                                </NativeText>)}
+                        </NativeText>
+                    </View>;
                 })}
-            </Text>
+            </View>
         </ScrollView>
         <View
             style={[
                 stylesheet.stickyActionContainer,
                 {
-                    transform: [
-                        {
-                            translateY: stickyTop
-                        }
-                    ],
-                    marginLeft: spaces.spacingMd
+                    marginLeft: spaces.spacingMd,
+                    ...webStyle({
+                        position: "sticky",
+                        top: spaces.spacingMd
+                    })
                 }
             ]}
         >

+ 55 - 0
src/components/codeViewer/parser.ts

@@ -649,3 +649,58 @@ export const parseCode = (code: string, language?: string): Token[] => {
 
     return tokens;
 };
+
+export const splitTokensIntoLines = (tokens: Token[]): Token[][] => {
+    const lines: Token[][] = [];
+    let currentLine: Token[] = [];
+
+    tokens.forEach((token) => {
+        if (token.value.includes("\n")) {
+            const parts = token.value.split("\n");
+            parts.forEach((part, index) => {
+                if (part.length > 0) {
+                    currentLine.push({
+                        ...token,
+                        value: part
+                    });
+                }
+
+                if (index < parts.length - 1) {
+                    lines.push(currentLine);
+
+                    currentLine = [];
+                }
+            });
+        } else {
+            currentLine.push(token);
+        }
+    });
+    if (currentLine.length > 0) {
+        lines.push(currentLine);
+    }
+    return lines;
+};
+
+export const getLineIndentation = (lineTokens: Token[]): number => {
+    let indentation = 0;
+    for (const token of lineTokens) {
+        if (token.type === "text") {
+            const match = token.value.match(/^(\s+)/);
+
+            if (match) {
+                const matchLen = (match[1] as string).length;
+
+                indentation += matchLen;
+
+                if (matchLen !== token.value.length) {
+                    break;
+                }
+            } else if (token.value.length > 0) {
+                break;
+            }
+        } else {
+            break;
+        }
+    }
+    return indentation;
+};

+ 74 - 2
src/components/codeViewer/stylesheet.ts

@@ -15,7 +15,6 @@ const stylesheet = StyleSheet.create({
     container: {
         alignItems: "flex-start",
         flexDirection: "row",
-        overflow: "hidden",
         borderRadius: 10,
         borderWidth: 1
     },
@@ -34,14 +33,71 @@ const stylesheet = StyleSheet.create({
         ...webStyle({
             whiteSpace: Platform.OS === "web" ? "pre" : "pre-wrap"
         })
+    },
+    lineRow: {
+        alignItems: "flex-start",
+        flexDirection: "row",
+        minHeight: 24
+    },
+    gutter: {
+        justifyContent: "flex-end",
+        flexDirection: "row",
+        alignItems: "center",
+        borderRightWidth: 1,
+        ...webStyle({
+            userSelect: "none"
+        })
+    },
+    lineNumber: {
+        textAlign: "right",
+        marginRight: 4,
+        fontSize: 12,
+        ...webStyle({
+            userSelect: "none"
+        })
+    },
+    foldIcon: {
+        justifyContent: "center",
+        alignItems: "center",
+        height: 14,
+        width: 14
+    },
+    foldIconPlaceholder: {
+        height: 14,
+        width: 14
     }
 });
 
 export const useStyles = ({
+    isLineNumberVisible = true,
+    isCollapsible = true,
     typography,
     colors,
     spaces
 }: ICodeViewerDynamicStyleProps) => {
+    const isGutterVisible = isLineNumberVisible || isCollapsible;
+
+    let gutterPaddingRight = 10;
+    let gutterMarginRight = 10;
+
+    let gutterWidth = 50;
+
+    if (isLineNumberVisible && !isCollapsible) {
+        gutterPaddingRight = 6;
+        gutterMarginRight = 8;
+
+        gutterWidth = 32;
+    } else if (!isLineNumberVisible && isCollapsible) {
+        gutterWidth = 30;
+    } else if (!isGutterVisible) {
+        gutterPaddingRight = 0;
+        gutterMarginRight = 0;
+
+        gutterWidth = 0;
+    }
+
+    const languageMarginLeft = spaces.spacingSm;
+
     const styles = {
         container: {
             backgroundColor: colors.content.container.subtle,
@@ -52,7 +108,23 @@ export const useStyles = ({
         text: {
             ...typography.bodyMediumSize,
             lineHeight: 24
-        } as TextStyle
+        } as TextStyle,
+        languageText: {
+            color: colors.content.text.low,
+            marginBottom: spaces.spacingSm,
+            marginLeft: languageMarginLeft,
+            ...webStyle({
+                userSelect: "none"
+            })
+        } as TextStyle,
+        gutter: {
+            paddingRight: gutterPaddingRight,
+            marginRight: gutterMarginRight,
+            width: gutterWidth
+        } as ViewStyle,
+        lineRow: {
+            marginLeft: !isGutterVisible ? spaces.spacingSm : 0
+        } as ViewStyle
     };
 
     return styles;

+ 6 - 0
src/components/codeViewer/type.ts

@@ -9,9 +9,15 @@ export interface ICodeViewerDynamicStyleProps {
     typography: NCoreUIKit.ActivePalette["typography"];
     spaces: NCoreUIKit.ActivePalette["spaces"];
     colors: NCoreUIKit.ActivePalette["colors"];
+    isLineNumberVisible?: boolean;
+    isCollapsible?: boolean;
 }
 
 interface ICodeViewerProps {
+    isLineNumberVisible?: boolean;
+    autoFoldMaxLines?: number;
+    isCollapsible?: boolean;
+    autoFold?: boolean;
     language?: string;
     code: string;
 }

+ 5 - 5
src/context/theme.tsx

@@ -71,13 +71,13 @@ class NCoreUIKitTheme<T extends ThemesType> extends NCoreContext<ThemeContextTyp
             initialSelectedTheme
         } = initialState;
 
-        const initialPalette = defaultPaletteData.palettes.find(p => p.name === (initialSelectedPalette ?? "nibgat"));
+        const initialPalette = defaultPaletteData.palettes.find(p => p.name === (initialSelectedPalette ?? "nibgat")) ?? defaultPaletteData.palettes[0]!;
 
-        if(!initialPalette) {
-            throw new Error("Initial Theme error!.");
-        }
+        let initialTheme = initialPalette.themes[initialSelectedTheme as keyof typeof initialPalette.themes];
 
-        const initialTheme = initialPalette.themes[initialSelectedTheme ?? "light"];
+        if(!initialTheme) {
+            initialTheme = initialPalette.themes.dark ?? Object.values(initialPalette.themes)[0]!;
+        }
 
         super({
             paletteKeys: defaultPaletteData.palettes.map(p => p.name) as Array<keyof NCoreUIKit.PaletteKey>,

+ 25 - 17
src/helpers/theme/palette.ts

@@ -8,17 +8,20 @@ const mergeCurrent = (defaultJSON: RecursiveRecord, projectJSON: RecursiveRecord
     const newJSON = JSON.parse(JSON.stringify(defaultJSON));
 
     projectKeys.forEach((pKey) => {
-        if(typeof projectJSON[pKey] === "string") {
-            newJSON[pKey] = projectJSON[pKey];
+        const pValue = projectJSON[pKey];
+        const dValue = defaultJSON[pKey];
+
+        if(typeof pValue === "string") {
+            newJSON[pKey] = pValue;
         } else {
-            if(typeof defaultJSON[pKey] !== "string") {
-                if(defaultJSON[pKey]) {
-                    newJSON[pKey] = mergeCurrent(defaultJSON[pKey], projectJSON[pKey]!);
+            if(typeof dValue !== "string") {
+                if(dValue) {
+                    newJSON[pKey] = mergeCurrent(dValue, pValue as RecursiveRecord);
                 } else {
-                    newJSON[pKey] = projectJSON[pKey];
+                    newJSON[pKey] = pValue;
                 }
             } else {
-                throw new Error("The project theme scheme is not valid.");
+                newJSON[pKey] = dValue;
             }
         }
     });
@@ -58,9 +61,10 @@ export const mergePalettes = (
                 return projectTheme;
             }
 
-            throw new Error("Theme not found!.");
+            return Object.values(projectPalette.themes)[0] as unknown as NCoreUIKit.ThemeTokens;
         } else {
-            throw new Error("Palette not found!.");
+            const fallbackP = defaultPalettes[0]!;
+            return (fallbackP.themes.dark ?? Object.values(fallbackP.themes)[0]) as unknown as NCoreUIKit.ThemeTokens;
         }
     }
 
@@ -68,25 +72,29 @@ export const mergePalettes = (
 
     if(projectPalette) {
         const defaultThemeKey = Object.keys(defaultPalette.themes).find(t => t === activeTheme) as keyof typeof defaultPalette.themes;
+        let defaultTheme = defaultPalette.themes[defaultThemeKey];
 
-        if(!defaultThemeKey) {
-            throw new Error("Theme not found!.");
+        if(!defaultTheme) {
+            defaultTheme = defaultPalette.themes.dark ?? Object.values(defaultPalette.themes)[0];
         }
 
-        const defaultTheme = defaultPalette.themes[defaultThemeKey as keyof typeof defaultPalette.themes] as unknown as NCoreUIKit.ThemeTokens;
-
         const projectThemeKey = Object.keys(projectPalette.themes).find(t => t === activeTheme) as keyof typeof projectPalette.themes;
 
         if(projectThemeKey) {
             const projectTheme = projectPalette.themes[projectThemeKey] as unknown as NCoreUIKit.ThemeTokens;
 
-            return mergeTheme(defaultTheme, projectTheme);
+            return mergeTheme(defaultTheme as unknown as NCoreUIKit.ThemeTokens, projectTheme);
         }
 
-        return defaultTheme;
+        return defaultTheme as unknown as NCoreUIKit.ThemeTokens;
     }
 
-    const activeT = defaultPalette.themes[activeTheme as keyof typeof defaultPalette.themes] as unknown as NCoreUIKit.ThemeTokens;
+    const defaultThemeKey = Object.keys(defaultPalette.themes).find(t => t === activeTheme) as keyof typeof defaultPalette.themes;
+    let activeT = defaultPalette.themes[defaultThemeKey];
+
+    if(!activeT) {
+        activeT = defaultPalette.themes.dark ?? Object.values(defaultPalette.themes)[0];
+    }
 
-    return activeT;
+    return activeT as unknown as NCoreUIKit.ThemeTokens;
 };