From 9e5590eaac2cabfcdf410a64668dc9ac9b1e0d1f Mon Sep 17 00:00:00 2001 From: Garrison Snelling Date: Tue, 17 Jun 2025 13:55:30 -0500 Subject: [PATCH 1/9] initial extraction --- css/doc.go | 5 + css/internal/config/borders.yaml | 68 ++ css/internal/config/colors.yaml | 265 +++++ css/internal/config/effects.yaml | 121 +++ css/internal/config/layout.yaml | 72 ++ css/internal/config/position.yaml | 230 +++++ css/internal/config/sizing.yaml | 406 ++++++++ css/internal/config/spacing.yaml | 46 + css/internal/config/typography.yaml | 85 ++ css/internal/config_loader.go | 316 ++++++ css/internal/css_generator.go | 386 +++++++ css/internal/css_generator_test.go | 109 ++ css/internal/go_generator.go | 540 ++++++++++ css/internal/tool/main.go | 45 + css/utilities.go | 1489 +++++++++++++++++++++++++++ css/utilities_test.go | 330 ++++++ go.mod | 13 + go.sum | 10 + html/container.go | 43 + html/document.go | 44 + html/element.go | 150 +++ html/element_test.go | 190 ++++ html/form.go | 39 + html/list.go | 34 + html/media.go | 25 + html/table.go | 46 + html/text.go | 46 + 27 files changed, 5153 insertions(+) create mode 100644 css/doc.go create mode 100644 css/internal/config/borders.yaml create mode 100644 css/internal/config/colors.yaml create mode 100644 css/internal/config/effects.yaml create mode 100644 css/internal/config/layout.yaml create mode 100644 css/internal/config/position.yaml create mode 100644 css/internal/config/sizing.yaml create mode 100644 css/internal/config/spacing.yaml create mode 100644 css/internal/config/typography.yaml create mode 100644 css/internal/config_loader.go create mode 100644 css/internal/css_generator.go create mode 100644 css/internal/css_generator_test.go create mode 100644 css/internal/go_generator.go create mode 100644 css/internal/tool/main.go create mode 100644 css/utilities.go create mode 100644 css/utilities_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 html/container.go create mode 100644 html/document.go create mode 100644 html/element.go create mode 100644 html/element_test.go create mode 100644 html/form.go create mode 100644 html/list.go create mode 100644 html/media.go create mode 100644 html/table.go create mode 100644 html/text.go diff --git a/css/doc.go b/css/doc.go new file mode 100644 index 0000000..7b59020 --- /dev/null +++ b/css/doc.go @@ -0,0 +1,5 @@ +// Package css provides CSS utility functions for the ComputeSDK UI framework. +// The utilities are generated from YAML configuration files. +package css + +//go:generate go run ./internal/tool diff --git a/css/internal/config/borders.yaml b/css/internal/config/borders.yaml new file mode 100644 index 0000000..81c12b2 --- /dev/null +++ b/css/internal/config/borders.yaml @@ -0,0 +1,68 @@ +borders: + width: + scale: [0, 1, 2, 4, 8] + properties: + - name: border + prefix: border + css_property: "border-width: {value}px" + - name: border-t + prefix: border-t + css_property: "border-top-width: {value}px" + - name: border-r + prefix: border-r + css_property: "border-right-width: {value}px" + - name: border-b + prefix: border-b + css_property: "border-bottom-width: {value}px" + - name: border-l + prefix: border-l + css_property: "border-left-width: {value}px" + + radius: + scale: [0, 1, 2, 3, 4, 6, 8, 12, 16, 20, 24] + rem_multiplier: 0.25 + properties: + - name: rounded + prefix: rounded + css_property: "border-radius: {value}rem" + - name: rounded-t + prefix: rounded-t + css_property: "border-top-left-radius: {value}rem; border-top-right-radius: {value}rem" + - name: rounded-r + prefix: rounded-r + css_property: "border-top-right-radius: {value}rem; border-bottom-right-radius: {value}rem" + - name: rounded-b + prefix: rounded-b + css_property: "border-bottom-right-radius: {value}rem; border-bottom-left-radius: {value}rem" + - name: rounded-l + prefix: rounded-l + css_property: "border-top-left-radius: {value}rem; border-bottom-left-radius: {value}rem" + - name: rounded-tl + prefix: rounded-tl + css_property: "border-top-left-radius: {value}rem" + - name: rounded-tr + prefix: rounded-tr + css_property: "border-top-right-radius: {value}rem" + - name: rounded-br + prefix: rounded-br + css_property: "border-bottom-right-radius: {value}rem" + - name: rounded-bl + prefix: rounded-bl + css_property: "border-bottom-left-radius: {value}rem" + special: + - name: rounded-full + css_property: "border-radius: 9999px" + - name: rounded-none + css_property: "border-radius: 0" + + style: + - name: border-solid + css_property: "border-style: solid" + - name: border-dashed + css_property: "border-style: dashed" + - name: border-dotted + css_property: "border-style: dotted" + - name: border-double + css_property: "border-style: double" + - name: border-none + css_property: "border-style: none" \ No newline at end of file diff --git a/css/internal/config/colors.yaml b/css/internal/config/colors.yaml new file mode 100644 index 0000000..2323fc3 --- /dev/null +++ b/css/internal/config/colors.yaml @@ -0,0 +1,265 @@ +colors: + red: + 50: "#fef2f2" + 100: "#fee2e2" + 200: "#fecaca" + 300: "#fca5a5" + 400: "#f87171" + 500: "#ef4444" + 600: "#dc2626" + 700: "#b91c1c" + 800: "#991b1b" + 900: "#7f1d1d" + 950: "#450a0a" + orange: + 50: "#fff7ed" + 100: "#ffedd5" + 200: "#fed7aa" + 300: "#fdba74" + 400: "#fb923c" + 500: "#f97316" + 600: "#ea580c" + 700: "#c2410c" + 800: "#9a3412" + 900: "#7c2d12" + 950: "#431407" + amber: + 50: "#fffbeb" + 100: "#fef3c7" + 200: "#fde68a" + 300: "#fcd34d" + 400: "#fbbf24" + 500: "#f59e0b" + 600: "#d97706" + 700: "#b45309" + 800: "#92400e" + 900: "#78350f" + 950: "#451a03" + yellow: + 50: "#fefce8" + 100: "#fef9c3" + 200: "#fef08a" + 300: "#fde047" + 400: "#facc15" + 500: "#eab308" + 600: "#ca8a04" + 700: "#a16207" + 800: "#854d0e" + 900: "#713f12" + 950: "#422006" + lime: + 50: "#f7fee7" + 100: "#ecfccb" + 200: "#d9f99d" + 300: "#bef264" + 400: "#a3e635" + 500: "#84cc16" + 600: "#65a30d" + 700: "#4d7c0f" + 800: "#3f6212" + 900: "#365314" + 950: "#1a2e05" + green: + 50: "#f0fdf4" + 100: "#dcfce7" + 200: "#bbf7d0" + 300: "#86efac" + 400: "#4ade80" + 500: "#22c55e" + 600: "#16a34a" + 700: "#15803d" + 800: "#166534" + 900: "#14532d" + 950: "#052e16" + emerald: + 50: "#ecfdf5" + 100: "#d1fae5" + 200: "#a7f3d0" + 300: "#6ee7b7" + 400: "#34d399" + 500: "#10b981" + 600: "#059669" + 700: "#047857" + 800: "#065f46" + 900: "#064e3b" + 950: "#022c22" + teal: + 50: "#f0fdfa" + 100: "#ccfbf1" + 200: "#99f6e4" + 300: "#5eead4" + 400: "#2dd4bf" + 500: "#14b8a6" + 600: "#0d9488" + 700: "#0f766e" + 800: "#115e59" + 900: "#134e4a" + 950: "#042f2e" + cyan: + 50: "#ecfeff" + 100: "#cffafe" + 200: "#a5f3fc" + 300: "#67e8f9" + 400: "#22d3ee" + 500: "#06b6d4" + 600: "#0891b2" + 700: "#0e7490" + 800: "#155e75" + 900: "#164e63" + 950: "#083344" + sky: + 50: "#f0f9ff" + 100: "#e0f2fe" + 200: "#bae6fd" + 300: "#7dd3fc" + 400: "#38bdf8" + 500: "#0ea5e9" + 600: "#0284c7" + 700: "#0369a1" + 800: "#075985" + 900: "#0c4a6e" + 950: "#082f49" + blue: + 50: "#eff6ff" + 100: "#dbeafe" + 200: "#bfdbfe" + 300: "#93c5fd" + 400: "#60a5fa" + 500: "#3b82f6" + 600: "#2563eb" + 700: "#1d4ed8" + 800: "#1e40af" + 900: "#1e3a8a" + 950: "#172554" + indigo: + 50: "#eef2ff" + 100: "#e0e7ff" + 200: "#c7d2fe" + 300: "#a5b4fc" + 400: "#818cf8" + 500: "#6366f1" + 600: "#4f46e5" + 700: "#4338ca" + 800: "#3730a3" + 900: "#312e81" + 950: "#1e1b4b" + violet: + 50: "#f5f3ff" + 100: "#ede9fe" + 200: "#ddd6fe" + 300: "#c4b5fd" + 400: "#a78bfa" + 500: "#8b5cf6" + 600: "#7c3aed" + 700: "#6d28d9" + 800: "#5b21b6" + 900: "#4c1d95" + 950: "#2e1065" + purple: + 50: "#faf5ff" + 100: "#f3e8ff" + 200: "#e9d5ff" + 300: "#d8b4fe" + 400: "#c084fc" + 500: "#a855f7" + 600: "#9333ea" + 700: "#7e22ce" + 800: "#6b21a8" + 900: "#581c87" + 950: "#3b0764" + fuchsia: + 50: "#fdf4ff" + 100: "#fae8ff" + 200: "#f5d0fe" + 300: "#f0abfc" + 400: "#e879f9" + 500: "#d946ef" + 600: "#c026d3" + 700: "#a21caf" + 800: "#86198f" + 900: "#701a75" + 950: "#4a044e" + pink: + 50: "#fdf2f8" + 100: "#fce7f3" + 200: "#fbcfe8" + 300: "#f9a8d4" + 400: "#f472b6" + 500: "#ec4899" + 600: "#db2777" + 700: "#be185d" + 800: "#9f1239" + 900: "#831843" + 950: "#500724" + rose: + 50: "#fff1f2" + 100: "#ffe4e6" + 200: "#fecdd3" + 300: "#fda4af" + 400: "#fb7185" + 500: "#f43f5e" + 600: "#e11d48" + 700: "#be123c" + 800: "#9f1239" + 900: "#881337" + 950: "#4c0519" + slate: + 50: "#f8fafc" + 100: "#f1f5f9" + 200: "#e2e8f0" + 300: "#cbd5e1" + 400: "#94a3b8" + 500: "#64748b" + 600: "#475569" + 700: "#334155" + 800: "#1e293b" + 900: "#0f172a" + 950: "#020617" + gray: + 50: "#f9fafb" + 100: "#f3f4f6" + 200: "#e5e7eb" + 300: "#d1d5db" + 400: "#9ca3af" + 500: "#6b7280" + 600: "#4b5563" + 700: "#374151" + 800: "#1f2937" + 900: "#111827" + 950: "#030712" + zinc: + 50: "#fafafa" + 100: "#f4f4f5" + 200: "#e4e4e7" + 300: "#d4d4d8" + 400: "#a1a1aa" + 500: "#71717a" + 600: "#52525b" + 700: "#3f3f46" + 800: "#27272a" + 900: "#18181b" + 950: "#09090b" + neutral: + 50: "#fafafa" + 100: "#f5f5f5" + 200: "#e5e5e5" + 300: "#d4d4d4" + 400: "#a3a3a3" + 500: "#737373" + 600: "#525252" + 700: "#404040" + 800: "#262626" + 900: "#171717" + 950: "#0a0a0a" + stone: + 50: "#fafaf9" + 100: "#f5f5f4" + 200: "#e7e5e4" + 300: "#d6d3d1" + 400: "#a8a29e" + 500: "#78716c" + 600: "#57534e" + 700: "#44403c" + 800: "#292524" + 900: "#1c1917" + 950: "#0c0a09" \ No newline at end of file diff --git a/css/internal/config/effects.yaml b/css/internal/config/effects.yaml new file mode 100644 index 0000000..c5ac501 --- /dev/null +++ b/css/internal/config/effects.yaml @@ -0,0 +1,121 @@ +effects: + opacity: + values: + - name: "0" + value: "0" + - name: "5" + value: "0.05" + - name: "10" + value: "0.1" + - name: "20" + value: "0.2" + - name: "25" + value: "0.25" + - name: "30" + value: "0.3" + - name: "40" + value: "0.4" + - name: "50" + value: "0.5" + - name: "60" + value: "0.6" + - name: "70" + value: "0.7" + - name: "75" + value: "0.75" + - name: "80" + value: "0.8" + - name: "90" + value: "0.9" + - name: "95" + value: "0.95" + - name: "100" + value: "1" + shadow: + values: + - name: "sm" + value: "0 1px 2px 0 rgb(0 0 0 / 0.05)" + - name: "" + value: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)" + - name: "md" + value: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" + - name: "lg" + value: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" + - name: "xl" + value: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" + - name: "2xl" + value: "0 25px 50px -12px rgb(0 0 0 / 0.25)" + - name: "inner" + value: "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)" + - name: "none" + value: "0 0 #0000" + cursor: + values: + - name: "cursor-auto" + css_property: "cursor: auto" + - name: "cursor-default" + css_property: "cursor: default" + - name: "cursor-pointer" + css_property: "cursor: pointer" + - name: "cursor-wait" + css_property: "cursor: wait" + - name: "cursor-text" + css_property: "cursor: text" + - name: "cursor-move" + css_property: "cursor: move" + - name: "cursor-help" + css_property: "cursor: help" + - name: "cursor-not-allowed" + css_property: "cursor: not-allowed" + - name: "cursor-none" + css_property: "cursor: none" + - name: "cursor-context-menu" + css_property: "cursor: context-menu" + - name: "cursor-progress" + css_property: "cursor: progress" + - name: "cursor-cell" + css_property: "cursor: cell" + - name: "cursor-crosshair" + css_property: "cursor: crosshair" + - name: "cursor-vertical-text" + css_property: "cursor: vertical-text" + - name: "cursor-alias" + css_property: "cursor: alias" + - name: "cursor-copy" + css_property: "cursor: copy" + - name: "cursor-no-drop" + css_property: "cursor: no-drop" + - name: "cursor-grab" + css_property: "cursor: grab" + - name: "cursor-grabbing" + css_property: "cursor: grabbing" + user_select: + values: + - name: "select-none" + css_property: "user-select: none" + - name: "select-text" + css_property: "user-select: text" + - name: "select-all" + css_property: "user-select: all" + - name: "select-auto" + css_property: "user-select: auto" + pointer_events: + values: + - name: "pointer-events-none" + css_property: "pointer-events: none" + - name: "pointer-events-auto" + css_property: "pointer-events: auto" + visibility: + values: + - name: "visible" + css_property: "visibility: visible" + - name: "invisible" + css_property: "visibility: hidden" + - name: "collapse" + css_property: "visibility: collapse" + screen_readers: + values: + - name: "sr-only" + css_property: "position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0" + - name: "not-sr-only" + css_property: "position: static; width: auto; height: auto; padding: 0; margin: 0; overflow: visible; clip: auto; white-space: normal" \ No newline at end of file diff --git a/css/internal/config/layout.yaml b/css/internal/config/layout.yaml new file mode 100644 index 0000000..33933ba --- /dev/null +++ b/css/internal/config/layout.yaml @@ -0,0 +1,72 @@ +layout: + display: + - name: block + css_property: "display: block" + - name: flex + css_property: "display: flex" + - name: grid + css_property: "display: grid" + - name: hidden + css_property: "display: none" + - name: inline + css_property: "display: inline" + - name: inline-block + css_property: "display: inline-block" + - name: inline-flex + css_property: "display: inline-flex" + - name: inline-grid + css_property: "display: inline-grid" + +flexbox: + justify: + - name: justify-start + css_property: "justify-content: flex-start" + - name: justify-center + css_property: "justify-content: center" + - name: justify-end + css_property: "justify-content: flex-end" + - name: justify-between + css_property: "justify-content: space-between" + - name: justify-around + css_property: "justify-content: space-around" + - name: justify-evenly + css_property: "justify-content: space-evenly" + align: + - name: items-start + css_property: "align-items: flex-start" + - name: items-center + css_property: "align-items: center" + - name: items-end + css_property: "align-items: flex-end" + - name: items-stretch + css_property: "align-items: stretch" + - name: items-baseline + css_property: "align-items: baseline" + direction: + - name: flex-row + css_property: "flex-direction: row" + - name: flex-col + css_property: "flex-direction: column" + - name: flex-row-reverse + css_property: "flex-direction: row-reverse" + - name: flex-col-reverse + css_property: "flex-direction: column-reverse" + wrap: + - name: flex-wrap + css_property: "flex-wrap: wrap" + - name: flex-nowrap + css_property: "flex-wrap: nowrap" + - name: flex-wrap-reverse + css_property: "flex-wrap: wrap-reverse" + +grid: + cols: + scale: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + css_template: "grid-template-columns: repeat({value}, minmax(0, 1fr))" + rows: + scale: [1, 2, 3, 4, 5, 6] + css_template: "grid-template-rows: repeat({value}, minmax(0, 1fr))" + gap: + scale: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96] + rem_multiplier: 0.25 + css_template: "gap: {value}rem" \ No newline at end of file diff --git a/css/internal/config/position.yaml b/css/internal/config/position.yaml new file mode 100644 index 0000000..dd06b92 --- /dev/null +++ b/css/internal/config/position.yaml @@ -0,0 +1,230 @@ +position: + types: + - name: "static" + css_property: "position: static" + - name: "fixed" + css_property: "position: fixed" + - name: "absolute" + css_property: "position: absolute" + - name: "relative" + css_property: "position: relative" + - name: "sticky" + css_property: "position: sticky" + inset: + scale: + - name: "0" + value: "0" + - name: "px" + value: "1px" + - name: "0.5" + value: "0.125rem" + - name: "1" + value: "0.25rem" + - name: "1.5" + value: "0.375rem" + - name: "2" + value: "0.5rem" + - name: "2.5" + value: "0.625rem" + - name: "3" + value: "0.75rem" + - name: "3.5" + value: "0.875rem" + - name: "4" + value: "1rem" + - name: "5" + value: "1.25rem" + - name: "6" + value: "1.5rem" + - name: "7" + value: "1.75rem" + - name: "8" + value: "2rem" + - name: "9" + value: "2.25rem" + - name: "10" + value: "2.5rem" + - name: "11" + value: "2.75rem" + - name: "12" + value: "3rem" + - name: "14" + value: "3.5rem" + - name: "16" + value: "4rem" + - name: "20" + value: "5rem" + - name: "24" + value: "6rem" + - name: "28" + value: "7rem" + - name: "32" + value: "8rem" + - name: "36" + value: "9rem" + - name: "40" + value: "10rem" + - name: "44" + value: "11rem" + - name: "48" + value: "12rem" + - name: "52" + value: "13rem" + - name: "56" + value: "14rem" + - name: "60" + value: "15rem" + - name: "64" + value: "16rem" + - name: "72" + value: "18rem" + - name: "80" + value: "20rem" + - name: "96" + value: "24rem" + special: + - name: "auto" + value: "auto" + - name: "full" + value: "100%" + - name: "1/2" + value: "50%" + - name: "1/3" + value: "33.333333%" + - name: "2/3" + value: "66.666667%" + - name: "1/4" + value: "25%" + - name: "2/4" + value: "50%" + - name: "3/4" + value: "75%" + negative_scale: + - name: "-px" + value: "-1px" + - name: "-0.5" + value: "-0.125rem" + - name: "-1" + value: "-0.25rem" + - name: "-1.5" + value: "-0.375rem" + - name: "-2" + value: "-0.5rem" + - name: "-2.5" + value: "-0.625rem" + - name: "-3" + value: "-0.75rem" + - name: "-3.5" + value: "-0.875rem" + - name: "-4" + value: "-1rem" + - name: "-5" + value: "-1.25rem" + - name: "-6" + value: "-1.5rem" + - name: "-7" + value: "-1.75rem" + - name: "-8" + value: "-2rem" + - name: "-9" + value: "-2.25rem" + - name: "-10" + value: "-2.5rem" + - name: "-11" + value: "-2.75rem" + - name: "-12" + value: "-3rem" + - name: "-14" + value: "-3.5rem" + - name: "-16" + value: "-4rem" + - name: "-20" + value: "-5rem" + - name: "-24" + value: "-6rem" + - name: "-28" + value: "-7rem" + - name: "-32" + value: "-8rem" + - name: "-36" + value: "-9rem" + - name: "-40" + value: "-10rem" + - name: "-44" + value: "-11rem" + - name: "-48" + value: "-12rem" + - name: "-52" + value: "-13rem" + - name: "-56" + value: "-14rem" + - name: "-60" + value: "-15rem" + - name: "-64" + value: "-16rem" + - name: "-72" + value: "-18rem" + - name: "-80" + value: "-20rem" + - name: "-96" + value: "-24rem" + negative_special: + - name: "-full" + value: "-100%" + - name: "-1/2" + value: "-50%" + - name: "-1/3" + value: "-33.333333%" + - name: "-2/3" + value: "-66.666667%" + - name: "-1/4" + value: "-25%" + - name: "-2/4" + value: "-50%" + - name: "-3/4" + value: "-75%" + z_index: + values: + - name: "0" + value: "0" + - name: "10" + value: "10" + - name: "20" + value: "20" + - name: "30" + value: "30" + - name: "40" + value: "40" + - name: "50" + value: "50" + - name: "auto" + value: "auto" + negative_values: + - name: "-10" + value: "-10" + overflow: + types: + - name: "overflow-auto" + css_property: "overflow: auto" + - name: "overflow-hidden" + css_property: "overflow: hidden" + - name: "overflow-visible" + css_property: "overflow: visible" + - name: "overflow-scroll" + css_property: "overflow: scroll" + - name: "overflow-x-auto" + css_property: "overflow-x: auto" + - name: "overflow-x-hidden" + css_property: "overflow-x: hidden" + - name: "overflow-x-visible" + css_property: "overflow-x: visible" + - name: "overflow-x-scroll" + css_property: "overflow-x: scroll" + - name: "overflow-y-auto" + css_property: "overflow-y: auto" + - name: "overflow-y-hidden" + css_property: "overflow-y: hidden" + - name: "overflow-y-visible" + css_property: "overflow-y: visible" + - name: "overflow-y-scroll" + css_property: "overflow-y: scroll" \ No newline at end of file diff --git a/css/internal/config/sizing.yaml b/css/internal/config/sizing.yaml new file mode 100644 index 0000000..e741e7b --- /dev/null +++ b/css/internal/config/sizing.yaml @@ -0,0 +1,406 @@ +sizing: + width: + scale: + - name: "0" + value: "0" + - name: "px" + value: "1px" + - name: "0.5" + value: "0.125rem" + - name: "1" + value: "0.25rem" + - name: "1.5" + value: "0.375rem" + - name: "2" + value: "0.5rem" + - name: "2.5" + value: "0.625rem" + - name: "3" + value: "0.75rem" + - name: "3.5" + value: "0.875rem" + - name: "4" + value: "1rem" + - name: "5" + value: "1.25rem" + - name: "6" + value: "1.5rem" + - name: "7" + value: "1.75rem" + - name: "8" + value: "2rem" + - name: "9" + value: "2.25rem" + - name: "10" + value: "2.5rem" + - name: "11" + value: "2.75rem" + - name: "12" + value: "3rem" + - name: "14" + value: "3.5rem" + - name: "16" + value: "4rem" + - name: "20" + value: "5rem" + - name: "24" + value: "6rem" + - name: "28" + value: "7rem" + - name: "32" + value: "8rem" + - name: "36" + value: "9rem" + - name: "40" + value: "10rem" + - name: "44" + value: "11rem" + - name: "48" + value: "12rem" + - name: "52" + value: "13rem" + - name: "56" + value: "14rem" + - name: "60" + value: "15rem" + - name: "64" + value: "16rem" + - name: "72" + value: "18rem" + - name: "80" + value: "20rem" + - name: "96" + value: "24rem" + special: + - name: "auto" + value: "auto" + - name: "full" + value: "100%" + - name: "screen" + value: "100vw" + - name: "min" + value: "min-content" + - name: "max" + value: "max-content" + - name: "fit" + value: "fit-content" + fractions: + - name: "1/2" + value: "50%" + - name: "1/3" + value: "33.333333%" + - name: "2/3" + value: "66.666667%" + - name: "1/4" + value: "25%" + - name: "2/4" + value: "50%" + - name: "3/4" + value: "75%" + - name: "1/5" + value: "20%" + - name: "2/5" + value: "40%" + - name: "3/5" + value: "60%" + - name: "4/5" + value: "80%" + - name: "1/6" + value: "16.666667%" + - name: "2/6" + value: "33.333333%" + - name: "3/6" + value: "50%" + - name: "4/6" + value: "66.666667%" + - name: "5/6" + value: "83.333333%" + - name: "1/12" + value: "8.333333%" + - name: "2/12" + value: "16.666667%" + - name: "3/12" + value: "25%" + - name: "4/12" + value: "33.333333%" + - name: "5/12" + value: "41.666667%" + - name: "6/12" + value: "50%" + - name: "7/12" + value: "58.333333%" + - name: "8/12" + value: "66.666667%" + - name: "9/12" + value: "75%" + - name: "10/12" + value: "83.333333%" + - name: "11/12" + value: "91.666667%" + height: + scale: + - name: "0" + value: "0" + - name: "px" + value: "1px" + - name: "0.5" + value: "0.125rem" + - name: "1" + value: "0.25rem" + - name: "1.5" + value: "0.375rem" + - name: "2" + value: "0.5rem" + - name: "2.5" + value: "0.625rem" + - name: "3" + value: "0.75rem" + - name: "3.5" + value: "0.875rem" + - name: "4" + value: "1rem" + - name: "5" + value: "1.25rem" + - name: "6" + value: "1.5rem" + - name: "7" + value: "1.75rem" + - name: "8" + value: "2rem" + - name: "9" + value: "2.25rem" + - name: "10" + value: "2.5rem" + - name: "11" + value: "2.75rem" + - name: "12" + value: "3rem" + - name: "14" + value: "3.5rem" + - name: "16" + value: "4rem" + - name: "20" + value: "5rem" + - name: "24" + value: "6rem" + - name: "28" + value: "7rem" + - name: "32" + value: "8rem" + - name: "36" + value: "9rem" + - name: "40" + value: "10rem" + - name: "44" + value: "11rem" + - name: "48" + value: "12rem" + - name: "52" + value: "13rem" + - name: "56" + value: "14rem" + - name: "60" + value: "15rem" + - name: "64" + value: "16rem" + - name: "72" + value: "18rem" + - name: "80" + value: "20rem" + - name: "96" + value: "24rem" + special: + - name: "auto" + value: "auto" + - name: "full" + value: "100%" + - name: "screen" + value: "100vh" + - name: "min" + value: "min-content" + - name: "max" + value: "max-content" + - name: "fit" + value: "fit-content" + fractions: + - name: "1/2" + value: "50%" + - name: "1/3" + value: "33.333333%" + - name: "2/3" + value: "66.666667%" + - name: "1/4" + value: "25%" + - name: "2/4" + value: "50%" + - name: "3/4" + value: "75%" + - name: "1/5" + value: "20%" + - name: "2/5" + value: "40%" + - name: "3/5" + value: "60%" + - name: "4/5" + value: "80%" + - name: "1/6" + value: "16.666667%" + - name: "2/6" + value: "33.333333%" + - name: "3/6" + value: "50%" + - name: "4/6" + value: "66.666667%" + - name: "5/6" + value: "83.333333%" + max_width: + values: + - name: "0" + value: "0rem" + - name: "none" + value: "none" + - name: "xs" + value: "20rem" + - name: "sm" + value: "24rem" + - name: "md" + value: "28rem" + - name: "lg" + value: "32rem" + - name: "xl" + value: "36rem" + - name: "2xl" + value: "42rem" + - name: "3xl" + value: "48rem" + - name: "4xl" + value: "56rem" + - name: "5xl" + value: "64rem" + - name: "6xl" + value: "72rem" + - name: "7xl" + value: "80rem" + - name: "full" + value: "100%" + - name: "min" + value: "min-content" + - name: "max" + value: "max-content" + - name: "fit" + value: "fit-content" + - name: "prose" + value: "65ch" + - name: "screen-sm" + value: "640px" + - name: "screen-md" + value: "768px" + - name: "screen-lg" + value: "1024px" + - name: "screen-xl" + value: "1280px" + - name: "screen-2xl" + value: "1536px" + min_width: + values: + - name: "0" + value: "0" + - name: "full" + value: "100%" + - name: "min" + value: "min-content" + - name: "max" + value: "max-content" + - name: "fit" + value: "fit-content" + max_height: + scale: + - name: "0" + value: "0" + - name: "px" + value: "1px" + - name: "1" + value: "0.25rem" + - name: "2" + value: "0.5rem" + - name: "3" + value: "0.75rem" + - name: "4" + value: "1rem" + - name: "5" + value: "1.25rem" + - name: "6" + value: "1.5rem" + - name: "7" + value: "1.75rem" + - name: "8" + value: "2rem" + - name: "9" + value: "2.25rem" + - name: "10" + value: "2.5rem" + - name: "11" + value: "2.75rem" + - name: "12" + value: "3rem" + - name: "14" + value: "3.5rem" + - name: "16" + value: "4rem" + - name: "20" + value: "5rem" + - name: "24" + value: "6rem" + - name: "28" + value: "7rem" + - name: "32" + value: "8rem" + - name: "36" + value: "9rem" + - name: "40" + value: "10rem" + - name: "44" + value: "11rem" + - name: "48" + value: "12rem" + - name: "52" + value: "13rem" + - name: "56" + value: "14rem" + - name: "60" + value: "15rem" + - name: "64" + value: "16rem" + - name: "72" + value: "18rem" + - name: "80" + value: "20rem" + - name: "96" + value: "24rem" + special: + - name: "full" + value: "100%" + - name: "screen" + value: "100vh" + - name: "min" + value: "min-content" + - name: "max" + value: "max-content" + - name: "fit" + value: "fit-content" + - name: "none" + value: "none" + min_height: + values: + - name: "0" + value: "0" + - name: "full" + value: "100%" + - name: "screen" + value: "100vh" + - name: "min" + value: "min-content" + - name: "max" + value: "max-content" + - name: "fit" + value: "fit-content" \ No newline at end of file diff --git a/css/internal/config/spacing.yaml b/css/internal/config/spacing.yaml new file mode 100644 index 0000000..af67b09 --- /dev/null +++ b/css/internal/config/spacing.yaml @@ -0,0 +1,46 @@ +spacing: + scale: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 96] + rem_multiplier: 0.25 + properties: + - name: padding + prefix: p + css_property: "padding: {value}" + - name: padding-x + prefix: px + css_property: "padding-left: {value}; padding-right: {value}" + - name: padding-y + prefix: py + css_property: "padding-top: {value}; padding-bottom: {value}" + - name: padding-top + prefix: pt + css_property: "padding-top: {value}" + - name: padding-right + prefix: pr + css_property: "padding-right: {value}" + - name: padding-bottom + prefix: pb + css_property: "padding-bottom: {value}" + - name: padding-left + prefix: pl + css_property: "padding-left: {value}" + - name: margin + prefix: m + css_property: "margin: {value}" + - name: margin-x + prefix: mx + css_property: "margin-left: {value}; margin-right: {value}" + - name: margin-y + prefix: my + css_property: "margin-top: {value}; margin-bottom: {value}" + - name: margin-top + prefix: mt + css_property: "margin-top: {value}" + - name: margin-right + prefix: mr + css_property: "margin-right: {value}" + - name: margin-bottom + prefix: mb + css_property: "margin-bottom: {value}" + - name: margin-left + prefix: ml + css_property: "margin-left: {value}" \ No newline at end of file diff --git a/css/internal/config/typography.yaml b/css/internal/config/typography.yaml new file mode 100644 index 0000000..b205bc6 --- /dev/null +++ b/css/internal/config/typography.yaml @@ -0,0 +1,85 @@ +typography: + sizes: + xs: + size: "0.75rem" + line_height: "1rem" + sm: + size: "0.875rem" + line_height: "1.25rem" + base: + size: "1rem" + line_height: "1.5rem" + lg: + size: "1.125rem" + line_height: "1.75rem" + xl: + size: "1.25rem" + line_height: "1.75rem" + "2xl": + size: "1.5rem" + line_height: "2rem" + "3xl": + size: "1.875rem" + line_height: "2.25rem" + "4xl": + size: "2.25rem" + line_height: "2.5rem" + "5xl": + size: "3rem" + line_height: "1" + "6xl": + size: "3.75rem" + line_height: "1" + "7xl": + size: "4.5rem" + line_height: "1" + "8xl": + size: "6rem" + line_height: "1" + "9xl": + size: "8rem" + line_height: "1" + families: + - name: font-sans + css_property: "font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'" + - name: font-serif + css_property: "font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif" + - name: font-mono + css_property: "font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace" + align: + - name: text-left + css_property: "text-align: left" + - name: text-center + css_property: "text-align: center" + - name: text-right + css_property: "text-align: right" + - name: text-justify + css_property: "text-align: justify" + weight: + - name: font-thin + css_property: "font-weight: 100" + - name: font-extralight + css_property: "font-weight: 200" + - name: font-light + css_property: "font-weight: 300" + - name: font-normal + css_property: "font-weight: 400" + - name: font-medium + css_property: "font-weight: 500" + - name: font-semibold + css_property: "font-weight: 600" + - name: font-bold + css_property: "font-weight: 700" + - name: font-extrabold + css_property: "font-weight: 800" + - name: font-black + css_property: "font-weight: 900" + decoration: + - name: underline + css_property: "text-decoration-line: underline" + - name: overline + css_property: "text-decoration-line: overline" + - name: line-through + css_property: "text-decoration-line: line-through" + - name: no-underline + css_property: "text-decoration-line: none" \ No newline at end of file diff --git a/css/internal/config_loader.go b/css/internal/config_loader.go new file mode 100644 index 0000000..a931095 --- /dev/null +++ b/css/internal/config_loader.go @@ -0,0 +1,316 @@ +package internal + +import ( + "embed" + "fmt" + + "gopkg.in/yaml.v3" +) + +//go:embed config/*.yaml +var configFS embed.FS + +// Config structures for different utility types +type SpacingConfig struct { + Spacing struct { + Scale []int `yaml:"scale"` + RemMultiplier float64 `yaml:"rem_multiplier"` + Properties []struct { + Name string `yaml:"name"` + Prefix string `yaml:"prefix"` + CSSProperty string `yaml:"css_property"` + } `yaml:"properties"` + } `yaml:"spacing"` +} + +type ColorsConfig struct { + Colors map[string]map[string]string `yaml:"colors"` +} + +type LayoutConfig struct { + Layout struct { + Display []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"display"` + } `yaml:"layout"` + Flexbox struct { + Justify []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"justify"` + Align []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"align"` + Direction []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"direction"` + Wrap []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"wrap"` + } `yaml:"flexbox"` + Grid struct { + Cols struct { + Scale []int `yaml:"scale"` + CSSTemplate string `yaml:"css_template"` + } `yaml:"cols"` + Rows struct { + Scale []int `yaml:"scale"` + CSSTemplate string `yaml:"css_template"` + } `yaml:"rows"` + Gap struct { + Scale []int `yaml:"scale"` + RemMultiplier float64 `yaml:"rem_multiplier"` + CSSTemplate string `yaml:"css_template"` + } `yaml:"gap"` + } `yaml:"grid"` +} + +type TypographyConfig struct { + Typography struct { + Sizes map[string]struct { + Size string `yaml:"size"` + LineHeight string `yaml:"line_height"` + } `yaml:"sizes"` + Families []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"families"` + Align []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"align"` + Weight []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"weight"` + Decoration []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"decoration"` + } `yaml:"typography"` +} + +type BordersConfig struct { + Borders struct { + Width struct { + Scale []int `yaml:"scale"` + Properties []struct { + Name string `yaml:"name"` + Prefix string `yaml:"prefix"` + CSSProperty string `yaml:"css_property"` + } `yaml:"properties"` + } `yaml:"width"` + Radius struct { + Scale []int `yaml:"scale"` + RemMultiplier float64 `yaml:"rem_multiplier"` + Properties []struct { + Name string `yaml:"name"` + Prefix string `yaml:"prefix"` + CSSProperty string `yaml:"css_property"` + } `yaml:"properties"` + Special []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"special"` + } `yaml:"radius"` + Style []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"style"` + } `yaml:"borders"` +} + +type SizingConfig struct { + Sizing struct { + Width struct { + Scale []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"scale"` + Special []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"special"` + Fractions []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"fractions"` + } `yaml:"width"` + Height struct { + Scale []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"scale"` + Special []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"special"` + Fractions []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"fractions"` + } `yaml:"height"` + MaxWidth struct { + Values []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"values"` + } `yaml:"max_width"` + MinWidth struct { + Values []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"values"` + } `yaml:"min_width"` + MaxHeight struct { + Scale []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"scale"` + Special []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"special"` + } `yaml:"max_height"` + MinHeight struct { + Values []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"values"` + } `yaml:"min_height"` + } `yaml:"sizing"` +} + +type PositionConfig struct { + Position struct { + Types []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"types"` + Inset struct { + Scale []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"scale"` + Special []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"special"` + NegativeScale []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"negative_scale"` + NegativeSpecial []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"negative_special"` + } `yaml:"inset"` + ZIndex struct { + Values []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"values"` + NegativeValues []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"negative_values"` + } `yaml:"z_index"` + Overflow struct { + Types []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"types"` + } `yaml:"overflow"` + } `yaml:"position"` +} + +type EffectsConfig struct { + Effects struct { + Opacity struct { + Values []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"values"` + } `yaml:"opacity"` + Shadow struct { + Values []struct { + Name string `yaml:"name"` + Value string `yaml:"value"` + } `yaml:"values"` + } `yaml:"shadow"` + Cursor struct { + Values []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"values"` + } `yaml:"cursor"` + UserSelect struct { + Values []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"values"` + } `yaml:"user_select"` + PointerEvents struct { + Values []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"values"` + } `yaml:"pointer_events"` + Visibility struct { + Values []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"values"` + } `yaml:"visibility"` + ScreenReaders struct { + Values []struct { + Name string `yaml:"name"` + CSSProperty string `yaml:"css_property"` + } `yaml:"values"` + } `yaml:"screen_readers"` + } `yaml:"effects"` +} + +// LoadConfig loads and parses all configuration files +func LoadConfig() (*SpacingConfig, *ColorsConfig, *LayoutConfig, *TypographyConfig, *BordersConfig, *SizingConfig, *PositionConfig, *EffectsConfig, error) { + var spacing SpacingConfig + var colors ColorsConfig + var layout LayoutConfig + var typography TypographyConfig + var borders BordersConfig + var sizing SizingConfig + var position PositionConfig + var effects EffectsConfig + + configs := []struct { + filename string + target any + }{ + {"config/spacing.yaml", &spacing}, + {"config/colors.yaml", &colors}, + {"config/layout.yaml", &layout}, + {"config/typography.yaml", &typography}, + {"config/borders.yaml", &borders}, + {"config/sizing.yaml", &sizing}, + {"config/position.yaml", &position}, + {"config/effects.yaml", &effects}, + } + + for _, config := range configs { + data, err := configFS.ReadFile(config.filename) + if err != nil { + return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to read %s: %w", config.filename, err) + } + + if err := yaml.Unmarshal(data, config.target); err != nil { + return nil, nil, nil, nil, nil, nil, nil, nil, fmt.Errorf("failed to parse %s: %w", config.filename, err) + } + } + + return &spacing, &colors, &layout, &typography, &borders, &sizing, &position, &effects, nil +} + diff --git a/css/internal/css_generator.go b/css/internal/css_generator.go new file mode 100644 index 0000000..6931785 --- /dev/null +++ b/css/internal/css_generator.go @@ -0,0 +1,386 @@ +package internal + +import ( + "fmt" + "sort" + "strings" +) + +type Stylesheet struct { + rules map[string]string +} + +func NewStylesheet() *Stylesheet { + return &Stylesheet{ + rules: make(map[string]string), + } +} + +func (s *Stylesheet) AddRule(selector, properties string) { + s.rules[selector] = properties +} + +func (s *Stylesheet) GenerateCSS() string { + if len(s.rules) == 0 { + return "" + } + + var css strings.Builder + + // Sort selectors for consistent output + selectors := make([]string, 0, len(s.rules)) + for selector := range s.rules { + selectors = append(selectors, selector) + } + sort.Strings(selectors) + + for _, selector := range selectors { + properties := s.rules[selector] + css.WriteString(fmt.Sprintf("%s { %s }\n", selector, properties)) + } + + return css.String() +} + +// GenerateUtilities creates CSS rules using the new config-driven approach +func GenerateUtilities() *Stylesheet { + stylesheet, err := GenerateUtilitiesFromConfig() + if err != nil { + // Fallback to basic utilities if config loading fails + return generateBasicUtilities() + } + return stylesheet +} + +// GenerateUtilitiesFromConfig creates CSS rules from config files +func GenerateUtilitiesFromConfig() (*Stylesheet, error) { + spacing, colors, layout, typography, borders, sizing, position, effects, err := LoadConfig() + if err != nil { + return nil, err + } + + s := NewStylesheet() + + // Add base/reset styles for proper typography + s.AddRule("*", "box-sizing: border-box") + s.AddRule("body", "margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; font-size: 1rem; line-height: 1.5; color: #111827") + s.AddRule("h1, h2, h3, h4, h5, h6", "margin-top: 0; margin-bottom: 0.5rem; font-weight: 600") + s.AddRule("h1", "font-size: 2.25rem; line-height: 2.5rem") + s.AddRule("h2", "font-size: 1.875rem; line-height: 2.25rem") + s.AddRule("h3", "font-size: 1.5rem; line-height: 2rem") + s.AddRule("h4", "font-size: 1.25rem; line-height: 1.75rem") + s.AddRule("h5", "font-size: 1.125rem; line-height: 1.75rem") + s.AddRule("h6", "font-size: 1rem; line-height: 1.5rem") + s.AddRule("p", "margin-top: 0; margin-bottom: 1rem") + s.AddRule("ul, ol", "margin-top: 0; margin-bottom: 1rem; padding-left: 2rem") + s.AddRule("li", "margin-bottom: 0.25rem") + s.AddRule("a", "color: #2563eb; text-decoration: underline") + s.AddRule("a:hover", "color: #1d4ed8") + s.AddRule("strong, b", "font-weight: 600") + s.AddRule("code", "font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.875em; background-color: #f3f4f6; padding: 0.125rem 0.25rem; border-radius: 0.25rem") + s.AddRule("pre", "font-family: ui-monospace, SFMono-Regular, 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 0.875rem; line-height: 1.5rem; background-color: #f3f4f6; padding: 1rem; border-radius: 0.375rem; overflow-x: auto") + s.AddRule("pre code", "background-color: transparent; padding: 0") + + // Generate spacing utilities + for _, prop := range spacing.Spacing.Properties { + for _, size := range spacing.Spacing.Scale { + value := float64(size) * spacing.Spacing.RemMultiplier + className := fmt.Sprintf(".%s-%d", prop.Prefix, size) + + cssValue := strings.ReplaceAll(prop.CSSProperty, "{value}", fmt.Sprintf("%.2frem", value)) + s.AddRule(className, cssValue) + } + } + + // Generate color utilities + for colorName, shades := range colors.Colors { + for shade, hex := range shades { + bgClass := fmt.Sprintf(".bg-%s-%s", colorName, shade) + textClass := fmt.Sprintf(".text-%s-%s", colorName, shade) + + s.AddRule(bgClass, fmt.Sprintf("background-color: %s", hex)) + s.AddRule(textClass, fmt.Sprintf("color: %s", hex)) + } + } + + // Generate layout utilities + for _, display := range layout.Layout.Display { + s.AddRule(fmt.Sprintf(".%s", display.Name), display.CSSProperty) + } + + // Generate flexbox utilities + for _, justify := range layout.Flexbox.Justify { + s.AddRule(fmt.Sprintf(".%s", justify.Name), justify.CSSProperty) + } + for _, align := range layout.Flexbox.Align { + s.AddRule(fmt.Sprintf(".%s", align.Name), align.CSSProperty) + } + for _, direction := range layout.Flexbox.Direction { + s.AddRule(fmt.Sprintf(".%s", direction.Name), direction.CSSProperty) + } + for _, wrap := range layout.Flexbox.Wrap { + s.AddRule(fmt.Sprintf(".%s", wrap.Name), wrap.CSSProperty) + } + + // Generate grid utilities + for _, cols := range layout.Grid.Cols.Scale { + className := fmt.Sprintf(".grid-cols-%d", cols) + cssValue := strings.ReplaceAll(layout.Grid.Cols.CSSTemplate, "{value}", fmt.Sprintf("%d", cols)) + s.AddRule(className, cssValue) + } + for _, rows := range layout.Grid.Rows.Scale { + className := fmt.Sprintf(".grid-rows-%d", rows) + cssValue := strings.ReplaceAll(layout.Grid.Rows.CSSTemplate, "{value}", fmt.Sprintf("%d", rows)) + s.AddRule(className, cssValue) + } + for _, gap := range layout.Grid.Gap.Scale { + value := float64(gap) * layout.Grid.Gap.RemMultiplier + className := fmt.Sprintf(".gap-%d", gap) + cssValue := strings.ReplaceAll(layout.Grid.Gap.CSSTemplate, "{value}", fmt.Sprintf("%.2f", value)) + s.AddRule(className, cssValue) + } + + // Generate typography utilities + for sizeName, sizeConfig := range typography.Typography.Sizes { + className := fmt.Sprintf(".text-%s", sizeName) + cssValue := fmt.Sprintf("font-size: %s; line-height: %s", sizeConfig.Size, sizeConfig.LineHeight) + s.AddRule(className, cssValue) + } + for _, family := range typography.Typography.Families { + s.AddRule(fmt.Sprintf(".%s", family.Name), family.CSSProperty) + } + for _, align := range typography.Typography.Align { + s.AddRule(fmt.Sprintf(".%s", align.Name), align.CSSProperty) + } + for _, weight := range typography.Typography.Weight { + s.AddRule(fmt.Sprintf(".%s", weight.Name), weight.CSSProperty) + } + for _, decoration := range typography.Typography.Decoration { + s.AddRule(fmt.Sprintf(".%s", decoration.Name), decoration.CSSProperty) + } + + // Generate border utilities + for _, prop := range borders.Borders.Width.Properties { + for _, width := range borders.Borders.Width.Scale { + className := fmt.Sprintf(".%s-%d", prop.Prefix, width) + cssValue := strings.ReplaceAll(prop.CSSProperty, "{value}", fmt.Sprintf("%d", width)) + s.AddRule(className, cssValue) + } + } + for _, prop := range borders.Borders.Radius.Properties { + for _, radius := range borders.Borders.Radius.Scale { + value := float64(radius) * borders.Borders.Radius.RemMultiplier + className := fmt.Sprintf(".%s-%d", prop.Prefix, radius) + cssValue := strings.ReplaceAll(prop.CSSProperty, "{value}", fmt.Sprintf("%.2f", value)) + s.AddRule(className, cssValue) + } + } + for _, special := range borders.Borders.Radius.Special { + s.AddRule(fmt.Sprintf(".%s", special.Name), special.CSSProperty) + } + for _, style := range borders.Borders.Style { + s.AddRule(fmt.Sprintf(".%s", style.Name), style.CSSProperty) + } + + // Generate sizing utilities + // Width + for _, size := range sizing.Sizing.Width.Scale { + s.AddRule(fmt.Sprintf(".w-%s", size.Name), fmt.Sprintf("width: %s", size.Value)) + } + for _, size := range sizing.Sizing.Width.Special { + s.AddRule(fmt.Sprintf(".w-%s", size.Name), fmt.Sprintf("width: %s", size.Value)) + } + for _, size := range sizing.Sizing.Width.Fractions { + s.AddRule(fmt.Sprintf(".w-%s", size.Name), fmt.Sprintf("width: %s", size.Value)) + } + + // Height + for _, size := range sizing.Sizing.Height.Scale { + s.AddRule(fmt.Sprintf(".h-%s", size.Name), fmt.Sprintf("height: %s", size.Value)) + } + for _, size := range sizing.Sizing.Height.Special { + s.AddRule(fmt.Sprintf(".h-%s", size.Name), fmt.Sprintf("height: %s", size.Value)) + } + for _, size := range sizing.Sizing.Height.Fractions { + s.AddRule(fmt.Sprintf(".h-%s", size.Name), fmt.Sprintf("height: %s", size.Value)) + } + + // Max width + for _, size := range sizing.Sizing.MaxWidth.Values { + s.AddRule(fmt.Sprintf(".max-w-%s", size.Name), fmt.Sprintf("max-width: %s", size.Value)) + } + + // Min width + for _, size := range sizing.Sizing.MinWidth.Values { + s.AddRule(fmt.Sprintf(".min-w-%s", size.Name), fmt.Sprintf("min-width: %s", size.Value)) + } + + // Max height + for _, size := range sizing.Sizing.MaxHeight.Scale { + s.AddRule(fmt.Sprintf(".max-h-%s", size.Name), fmt.Sprintf("max-height: %s", size.Value)) + } + for _, size := range sizing.Sizing.MaxHeight.Special { + s.AddRule(fmt.Sprintf(".max-h-%s", size.Name), fmt.Sprintf("max-height: %s", size.Value)) + } + + // Min height + for _, size := range sizing.Sizing.MinHeight.Values { + s.AddRule(fmt.Sprintf(".min-h-%s", size.Name), fmt.Sprintf("min-height: %s", size.Value)) + } + + // Generate position utilities + for _, pos := range position.Position.Types { + s.AddRule(fmt.Sprintf(".%s", pos.Name), pos.CSSProperty) + } + + // Inset utilities (top, right, bottom, left) + directions := []string{"top", "right", "bottom", "left"} + for _, dir := range directions { + // Regular scale + for _, inset := range position.Position.Inset.Scale { + s.AddRule(fmt.Sprintf(".%s-%s", dir, inset.Name), fmt.Sprintf("%s: %s", dir, inset.Value)) + } + // Special values + for _, inset := range position.Position.Inset.Special { + s.AddRule(fmt.Sprintf(".%s-%s", dir, inset.Name), fmt.Sprintf("%s: %s", dir, inset.Value)) + } + // Negative scale + for _, inset := range position.Position.Inset.NegativeScale { + s.AddRule(fmt.Sprintf(".-%s%s", dir, inset.Name), fmt.Sprintf("%s: %s", dir, inset.Value)) + } + // Negative special + for _, inset := range position.Position.Inset.NegativeSpecial { + s.AddRule(fmt.Sprintf(".-%s%s", dir, inset.Name), fmt.Sprintf("%s: %s", dir, inset.Value)) + } + } + + // Inset (all sides) + for _, inset := range position.Position.Inset.Scale { + s.AddRule(fmt.Sprintf(".inset-%s", inset.Name), fmt.Sprintf("inset: %s", inset.Value)) + } + for _, inset := range position.Position.Inset.Special { + s.AddRule(fmt.Sprintf(".inset-%s", inset.Name), fmt.Sprintf("inset: %s", inset.Value)) + } + + // Inset X and Y + for _, inset := range position.Position.Inset.Scale { + s.AddRule(fmt.Sprintf(".inset-x-%s", inset.Name), fmt.Sprintf("left: %s; right: %s", inset.Value, inset.Value)) + s.AddRule(fmt.Sprintf(".inset-y-%s", inset.Name), fmt.Sprintf("top: %s; bottom: %s", inset.Value, inset.Value)) + } + for _, inset := range position.Position.Inset.Special { + s.AddRule(fmt.Sprintf(".inset-x-%s", inset.Name), fmt.Sprintf("left: %s; right: %s", inset.Value, inset.Value)) + s.AddRule(fmt.Sprintf(".inset-y-%s", inset.Name), fmt.Sprintf("top: %s; bottom: %s", inset.Value, inset.Value)) + } + + // Z-index + for _, z := range position.Position.ZIndex.Values { + s.AddRule(fmt.Sprintf(".z-%s", z.Name), fmt.Sprintf("z-index: %s", z.Value)) + } + for _, z := range position.Position.ZIndex.NegativeValues { + s.AddRule(fmt.Sprintf(".-z%s", z.Name), fmt.Sprintf("z-index: %s", z.Value)) + } + + // Overflow + for _, overflow := range position.Position.Overflow.Types { + s.AddRule(fmt.Sprintf(".%s", overflow.Name), overflow.CSSProperty) + } + + // Generate effects utilities + // Opacity + for _, opacity := range effects.Effects.Opacity.Values { + s.AddRule(fmt.Sprintf(".opacity-%s", opacity.Name), fmt.Sprintf("opacity: %s", opacity.Value)) + } + + // Shadow + for _, shadow := range effects.Effects.Shadow.Values { + className := ".shadow" + if shadow.Name != "" { + className = fmt.Sprintf(".shadow-%s", shadow.Name) + } + s.AddRule(className, fmt.Sprintf("box-shadow: %s", shadow.Value)) + } + + // Cursor + for _, cursor := range effects.Effects.Cursor.Values { + s.AddRule(fmt.Sprintf(".%s", cursor.Name), cursor.CSSProperty) + } + + // User select + for _, userSelect := range effects.Effects.UserSelect.Values { + s.AddRule(fmt.Sprintf(".%s", userSelect.Name), userSelect.CSSProperty) + } + + // Pointer events + for _, pointerEvents := range effects.Effects.PointerEvents.Values { + s.AddRule(fmt.Sprintf(".%s", pointerEvents.Name), pointerEvents.CSSProperty) + } + + // Visibility + for _, visibility := range effects.Effects.Visibility.Values { + s.AddRule(fmt.Sprintf(".%s", visibility.Name), visibility.CSSProperty) + } + + // Screen readers + for _, sr := range effects.Effects.ScreenReaders.Values { + s.AddRule(fmt.Sprintf(".%s", sr.Name), sr.CSSProperty) + } + + return s, nil +} + +// GenerateMinimalCSS creates CSS rules only for the specified classes +func GenerateMinimalCSS(usedClasses []string) *Stylesheet { + if len(usedClasses) == 0 { + return NewStylesheet() + } + + // Convert slice to map for faster lookup + usedClassMap := make(map[string]bool) + for _, class := range usedClasses { + usedClassMap[class] = true + } + + // Generate full stylesheet + fullStylesheet, err := GenerateUtilitiesFromConfig() + if err != nil { + return generateBasicUtilities() + } + + // Create minimal stylesheet with only used classes + minimalStylesheet := NewStylesheet() + + // Always include base styles (non-class selectors) + for selector, properties := range fullStylesheet.rules { + if !strings.HasPrefix(selector, ".") { + // This is a base style (element selector), always include it + minimalStylesheet.AddRule(selector, properties) + } else { + // This is a class selector, only include if used + className := strings.TrimPrefix(selector, ".") + if usedClassMap[className] { + minimalStylesheet.AddRule(selector, properties) + } + } + } + + return minimalStylesheet +} + +// generateBasicUtilities provides a fallback with basic utilities +func generateBasicUtilities() *Stylesheet { + s := NewStylesheet() + + // Basic spacing utilities (0-16) + for i := 0; i <= 16; i++ { + rem := float64(i) * 0.25 + s.AddRule(fmt.Sprintf(".p-%d", i), fmt.Sprintf("padding: %.2frem", rem)) + s.AddRule(fmt.Sprintf(".m-%d", i), fmt.Sprintf("margin: %.2frem", rem)) + } + + // Basic layout utilities + s.AddRule(".flex", "display: flex") + s.AddRule(".block", "display: block") + s.AddRule(".hidden", "display: none") + + return s +} \ No newline at end of file diff --git a/css/internal/css_generator_test.go b/css/internal/css_generator_test.go new file mode 100644 index 0000000..50a515e --- /dev/null +++ b/css/internal/css_generator_test.go @@ -0,0 +1,109 @@ +package internal_test + +import ( + "strings" + "testing" + + "github.com/computesdk/zforge/css/internal" + "github.com/stretchr/testify/assert" +) + +func TestNewStylesheet(t *testing.T) { + s := internal.NewStylesheet() + assert.NotNil(t, s) + assert.Empty(t, s.GenerateCSS()) +} + +func TestStylesheetAddRule(t *testing.T) { + s := internal.NewStylesheet() + + s.AddRule(".test-class", "color: red") + css := s.GenerateCSS() + + assert.Contains(t, css, ".test-class") + assert.Contains(t, css, "color: red") +} + +func TestStylesheetMultipleRules(t *testing.T) { + s := internal.NewStylesheet() + + s.AddRule(".class-a", "color: blue") + s.AddRule(".class-b", "background: white") + s.AddRule(".class-c", "margin: 10px") + + css := s.GenerateCSS() + + // Check all rules are present + assert.Contains(t, css, ".class-a { color: blue }") + assert.Contains(t, css, ".class-b { background: white }") + assert.Contains(t, css, ".class-c { margin: 10px }") + + // Verify output is sorted by selector + lines := strings.Split(strings.TrimSpace(css), "\n") + assert.Equal(t, 3, len(lines)) + assert.True(t, strings.HasPrefix(lines[0], ".class-a")) + assert.True(t, strings.HasPrefix(lines[1], ".class-b")) + assert.True(t, strings.HasPrefix(lines[2], ".class-c")) +} + +func TestStylesheetOverwriteRule(t *testing.T) { + s := internal.NewStylesheet() + + s.AddRule(".test", "color: red") + s.AddRule(".test", "color: blue") + + css := s.GenerateCSS() + + // Should only have the latest rule + assert.Contains(t, css, "color: blue") + assert.NotContains(t, css, "color: red") + + // Should only appear once + count := strings.Count(css, ".test") + assert.Equal(t, 1, count) +} + +func TestGenerateUtilities(t *testing.T) { + s := internal.GenerateUtilities() + assert.NotNil(t, s) + + css := s.GenerateCSS() + assert.NotEmpty(t, css) + + // Check for basic utilities that should be present + // either from config or fallback + expectedPatterns := []string{ + ".p-4", + ".m-4", + ".flex", + ".block", + ".hidden", + } + + for _, pattern := range expectedPatterns { + assert.Contains(t, css, pattern, "Expected CSS to contain %s", pattern) + } +} + +func TestStylesheetEmptyProperties(t *testing.T) { + s := internal.NewStylesheet() + + s.AddRule(".empty", "") + css := s.GenerateCSS() + + assert.Contains(t, css, ".empty { }") +} + +func TestStylesheetSpecialCharacters(t *testing.T) { + s := internal.NewStylesheet() + + s.AddRule(".class\\:hover", "color: red") + s.AddRule("#id-with-dash", "background: blue") + s.AddRule("[data-attr]", "display: none") + + css := s.GenerateCSS() + + assert.Contains(t, css, ".class\\:hover") + assert.Contains(t, css, "#id-with-dash") + assert.Contains(t, css, "[data-attr]") +} \ No newline at end of file diff --git a/css/internal/go_generator.go b/css/internal/go_generator.go new file mode 100644 index 0000000..9c15023 --- /dev/null +++ b/css/internal/go_generator.go @@ -0,0 +1,540 @@ +package internal + +import ( + "fmt" + "strings" + "text/template" +) + +// CodeGenerator generates Go utility functions from config +type CodeGenerator struct { + functions []string +} + +func NewCodeGenerator() *CodeGenerator { + return &CodeGenerator{ + functions: make([]string, 0), + } +} + +// AddFunction adds a utility function to be generated +func (cg *CodeGenerator) AddFunction(funcCode string) { + cg.functions = append(cg.functions, funcCode) +} + +// GenerateSpacingFunctions creates functions from spacing config +func (cg *CodeGenerator) GenerateSpacingFunctions(spacing *SpacingConfig) { + for _, prop := range spacing.Spacing.Properties { + // Generate function like: func P(size int) Class { return Class(fmt.Sprintf("p-%d", size)) } + funcName := strings.Title(prop.Prefix) + if len(funcName) > 1 && funcName[1:2] != strings.ToUpper(funcName[1:2]) { + // Handle cases like "px" -> "Px" + funcName = strings.ToUpper(prop.Prefix[:1]) + prop.Prefix[1:] + } + + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s(size int) Class { + className := fmt.Sprintf("%s-%%d", size) + trackClass(className) + return Class(className) +}`, prop.Name, prop.Name, funcName, prop.Prefix) + + cg.AddFunction(funcCode) + } +} + +// GenerateColorFunctions creates functions from colors config +func (cg *CodeGenerator) GenerateColorFunctions(colors *ColorsConfig) { + for colorName := range colors.Colors { + // Generate BgColorName and TextColorName functions + bgFuncName := fmt.Sprintf("Bg%s", strings.Title(colorName)) + textFuncName := fmt.Sprintf("Text%s", strings.Title(colorName)) + + bgFunc := fmt.Sprintf(`// %s applies bg-%s-shade utility +func %s(shade int) Class { + className := fmt.Sprintf("bg-%s-%%d", shade) + trackClass(className) + return Class(className) +}`, bgFuncName, colorName, bgFuncName, colorName) + + textFunc := fmt.Sprintf(`// %s applies text-%s-shade utility +func %s(shade int) Class { + className := fmt.Sprintf("text-%s-%%d", shade) + trackClass(className) + return Class(className) +}`, textFuncName, colorName, textFuncName, colorName) + + cg.AddFunction(bgFunc) + cg.AddFunction(textFunc) + } +} + +// GenerateLayoutFunctions creates functions from layout config +func (cg *CodeGenerator) GenerateLayoutFunctions(layout *LayoutConfig) { + // Display utilities + for _, display := range layout.Layout.Display { + funcName := toCamelCase(display.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, display.Name, funcName, display.Name, display.Name) + cg.AddFunction(funcCode) + } + + // Flexbox utilities + for _, justify := range layout.Flexbox.Justify { + funcName := toCamelCase(justify.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, justify.Name, funcName, justify.Name, justify.Name) + cg.AddFunction(funcCode) + } + + for _, align := range layout.Flexbox.Align { + funcName := toCamelCase(align.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, align.Name, funcName, align.Name, align.Name) + cg.AddFunction(funcCode) + } + + for _, direction := range layout.Flexbox.Direction { + funcName := toCamelCase(direction.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, direction.Name, funcName, direction.Name, direction.Name) + cg.AddFunction(funcCode) + } +} + +// GenerateTypographyFunctions creates functions from typography config +func (cg *CodeGenerator) GenerateTypographyFunctions(typography *TypographyConfig) { + // Text size utilities + for sizeName := range typography.Typography.Sizes { + funcName := fmt.Sprintf("Text%s", strings.Title(sizeName)) + if sizeName == "2xl" || sizeName == "3xl" { // Handle special cases + funcName = fmt.Sprintf("Text%s", strings.ToUpper(sizeName)) + } + funcCode := fmt.Sprintf(`// %s applies text-%s utility +func %s() Class { + trackClass("text-%s") + return "text-%s" +}`, funcName, sizeName, funcName, sizeName, sizeName) + cg.AddFunction(funcCode) + } + + // Text alignment utilities + for _, align := range typography.Typography.Align { + funcName := toCamelCase(align.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, align.Name, funcName, align.Name, align.Name) + cg.AddFunction(funcCode) + } + + // Font weight utilities + for _, weight := range typography.Typography.Weight { + funcName := toCamelCase(weight.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, weight.Name, funcName, weight.Name, weight.Name) + cg.AddFunction(funcCode) + } + + // Font family utilities + for _, family := range typography.Typography.Families { + funcName := toCamelCase(family.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, family.Name, funcName, family.Name, family.Name) + cg.AddFunction(funcCode) + } +} + +// GenerateBorderFunctions creates functions from borders config +func (cg *CodeGenerator) GenerateBorderFunctions(borders *BordersConfig) { + // Border width utilities + for _, prop := range borders.Borders.Width.Properties { + funcName := toCamelCase(prop.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s(width int) Class { + className := fmt.Sprintf("%s-%%d", width) + trackClass(className) + return Class(className) +}`, funcName, prop.Name, funcName, prop.Prefix) + cg.AddFunction(funcCode) + } + + // Border radius utilities + for _, prop := range borders.Borders.Radius.Properties { + funcName := toCamelCase(prop.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s(radius int) Class { + className := fmt.Sprintf("%s-%%d", radius) + trackClass(className) + return Class(className) +}`, funcName, prop.Name, funcName, prop.Prefix) + cg.AddFunction(funcCode) + } + + // Special border radius utilities + for _, special := range borders.Borders.Radius.Special { + funcName := toCamelCase(special.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, special.Name, funcName, special.Name, special.Name) + cg.AddFunction(funcCode) + } +} + +// GenerateGoCode creates the complete utilities.go file content +func (cg *CodeGenerator) GenerateGoCode() string { + tmpl := `// Code generated from YAML configs. DO NOT EDIT. +package css + +import ( + "fmt" + "sync" + "github.com/heysnelling/computesdk/pkg/ui/css/internal" +) + +type Class string + +func (c Class) String() string { + return string(c) +} + +// Global class tracker +var ( + usedClasses = make(map[string]bool) + classMutex sync.RWMutex +) + +// trackClass registers a class as being used +func trackClass(className string) { + classMutex.Lock() + defer classMutex.Unlock() + usedClasses[className] = true +} + +// GetUsedClasses returns a slice of all tracked classes +func GetUsedClasses() []string { + classMutex.RLock() + defer classMutex.RUnlock() + + classes := make([]string, 0, len(usedClasses)) + for class := range usedClasses { + classes = append(classes, class) + } + return classes +} + +// ResetTracking clears all tracked classes +func ResetTracking() { + classMutex.Lock() + defer classMutex.Unlock() + usedClasses = make(map[string]bool) +} + +// Stylesheet wraps the internal stylesheet type +type Stylesheet struct { + internal interface{ GenerateCSS() string } +} + +// Generate returns the CSS string +func (s *Stylesheet) Generate() string { + if s.internal == nil { + return "" + } + if gen, ok := s.internal.(interface{ GenerateCSS() string }); ok { + return gen.GenerateCSS() + } + return "" +} + +// GenerateUtilities creates CSS rules using the config-driven approach +func GenerateUtilities() *Stylesheet { + return &Stylesheet{internal: internal.GenerateUtilities()} +} + +// GenerateMinimalCSS generates CSS only for tracked classes +func GenerateMinimalCSS() *Stylesheet { + return &Stylesheet{internal: internal.GenerateMinimalCSS(GetUsedClasses())} +} + +{{range .Functions}} +{{.}} + +{{end}}` + + t := template.Must(template.New("utilities").Parse(tmpl)) + var buf strings.Builder + + data := struct { + Functions []string + }{ + Functions: cg.functions, + } + + t.Execute(&buf, data) + return buf.String() +} + +// Helper function to convert kebab-case to CamelCase +func toCamelCase(input string) string { + parts := strings.Split(input, "-") + result := "" + for _, part := range parts { + if part != "" { + result += strings.Title(part) + } + } + // Handle special cases for valid Go identifiers + result = strings.ReplaceAll(result, "-", "") + return result +} + +// GenerateSizingFunctions creates functions from sizing config +func (cg *CodeGenerator) GenerateSizingFunctions(sizing *SizingConfig) { + // Width utilities with fractions + funcCode := `// W applies width utility +func W(size string) Class { + className := fmt.Sprintf("w-%s", size) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Height utilities with fractions + funcCode = `// H applies height utility +func H(size string) Class { + className := fmt.Sprintf("h-%s", size) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Max width utilities + funcCode = `// MaxW applies max-width utility +func MaxW(size string) Class { + className := fmt.Sprintf("max-w-%s", size) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Min width utilities + funcCode = `// MinW applies min-width utility +func MinW(size string) Class { + className := fmt.Sprintf("min-w-%s", size) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Max height utilities + funcCode = `// MaxH applies max-height utility +func MaxH(size string) Class { + className := fmt.Sprintf("max-h-%s", size) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Min height utilities + funcCode = `// MinH applies min-height utility +func MinH(size string) Class { + className := fmt.Sprintf("min-h-%s", size) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) +} + +// GeneratePositionFunctions creates functions from position config +func (cg *CodeGenerator) GeneratePositionFunctions(position *PositionConfig) { + // Position type utilities + for _, pos := range position.Position.Types { + funcName := toCamelCase(pos.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, pos.Name, funcName, pos.Name, pos.Name) + cg.AddFunction(funcCode) + } + + // Inset utilities (generic functions for values that can be positive or negative) + directions := []string{"Top", "Right", "Bottom", "Left"} + for _, dir := range directions { + funcCode := fmt.Sprintf(`// %s applies %s position utility +func %s(value string) Class { + className := fmt.Sprintf("%s-%%s", value) + trackClass(className) + return Class(className) +}`, dir, strings.ToLower(dir), dir, strings.ToLower(dir)) + cg.AddFunction(funcCode) + } + + // Inset utilities + funcCode := `// Inset applies inset utility +func Inset(value string) Class { + className := fmt.Sprintf("inset-%s", value) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + funcCode = `// InsetX applies horizontal inset utility +func InsetX(value string) Class { + className := fmt.Sprintf("inset-x-%s", value) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + funcCode = `// InsetY applies vertical inset utility +func InsetY(value string) Class { + className := fmt.Sprintf("inset-y-%s", value) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Z-index utilities + funcCode = `// Z applies z-index utility +func Z(value string) Class { + className := fmt.Sprintf("z-%s", value) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Overflow utilities + for _, overflow := range position.Position.Overflow.Types { + funcName := toCamelCase(overflow.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, overflow.Name, funcName, overflow.Name, overflow.Name) + cg.AddFunction(funcCode) + } +} + +// GenerateEffectsFunctions creates functions from effects config +func (cg *CodeGenerator) GenerateEffectsFunctions(effects *EffectsConfig) { + // Opacity utilities + funcCode := `// Opacity applies opacity utility +func Opacity(value int) Class { + className := fmt.Sprintf("opacity-%d", value) + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Shadow utilities + funcCode = `// Shadow applies shadow utility +func Shadow(size ...string) Class { + var className string + if len(size) > 0 && size[0] != "" { + className = fmt.Sprintf("shadow-%s", size[0]) + } else { + className = "shadow" + } + trackClass(className) + return Class(className) +}` + cg.AddFunction(funcCode) + + // Cursor utilities + for _, cursor := range effects.Effects.Cursor.Values { + funcName := toCamelCase(cursor.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, cursor.Name, funcName, cursor.Name, cursor.Name) + cg.AddFunction(funcCode) + } + + // User select utilities + for _, userSelect := range effects.Effects.UserSelect.Values { + funcName := toCamelCase(userSelect.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, userSelect.Name, funcName, userSelect.Name, userSelect.Name) + cg.AddFunction(funcCode) + } + + // Pointer events utilities + for _, pointerEvents := range effects.Effects.PointerEvents.Values { + funcName := toCamelCase(pointerEvents.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, pointerEvents.Name, funcName, pointerEvents.Name, pointerEvents.Name) + cg.AddFunction(funcCode) + } + + // Visibility utilities + for _, visibility := range effects.Effects.Visibility.Values { + funcName := toCamelCase(visibility.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, visibility.Name, funcName, visibility.Name, visibility.Name) + cg.AddFunction(funcCode) + } + + // Screen reader utilities + for _, sr := range effects.Effects.ScreenReaders.Values { + funcName := toCamelCase(sr.Name) + funcCode := fmt.Sprintf(`// %s applies %s utility +func %s() Class { + trackClass("%s") + return "%s" +}`, funcName, sr.Name, funcName, sr.Name, sr.Name) + cg.AddFunction(funcCode) + } +} + +// GenerateUtilitiesCode generates the complete utilities.go from all configs +func GenerateUtilitiesCode() (string, error) { + spacing, colors, layout, typography, borders, sizing, position, effects, err := LoadConfig() + if err != nil { + return "", err + } + + cg := NewCodeGenerator() + + cg.GenerateSpacingFunctions(spacing) + cg.GenerateColorFunctions(colors) + cg.GenerateLayoutFunctions(layout) + cg.GenerateTypographyFunctions(typography) + cg.GenerateBorderFunctions(borders) + cg.GenerateSizingFunctions(sizing) + cg.GeneratePositionFunctions(position) + cg.GenerateEffectsFunctions(effects) + + return cg.GenerateGoCode(), nil +} \ No newline at end of file diff --git a/css/internal/tool/main.go b/css/internal/tool/main.go new file mode 100644 index 0000000..9e1ab97 --- /dev/null +++ b/css/internal/tool/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/computesdk/zforge/css/internal" +) + +func main() { + fmt.Println("🚀 Generating CSS utilities from YAML configs...") + + code, err := internal.GenerateUtilitiesCode() + if err != nil { + fmt.Printf("❌ Error generating utilities: %v\n", err) + os.Exit(1) + } + + // Debug: print current working directory + cwd, _ := os.Getwd() + fmt.Printf("Current working directory: %s\n", cwd) + + // Write to the css package directory (up from tool/ to internal/, up to css/) + outputPath := "../../utilities.go" + absPath, _ := filepath.Abs(outputPath) + fmt.Printf("Writing to: %s\n", absPath) + + err = os.WriteFile(outputPath, []byte(code), 0644) + if err != nil { + fmt.Printf("❌ Error writing utilities.go: %v\n", err) + os.Exit(1) + } + + fmt.Println("✅ Generated utilities.go successfully!") + fmt.Printf("📊 Generated %d lines of Go code from YAML configs\n", len(code)) + + // Count utilities generated + spacing, colors, _, _, _, _, _, _, _ := internal.LoadConfig() + if spacing != nil && colors != nil { + colorCount := len(colors.Colors) + spacingCount := len(spacing.Spacing.Properties) + fmt.Printf("🎨 %d color palettes, %d spacing utilities\n", colorCount, spacingCount) + } +} diff --git a/css/utilities.go b/css/utilities.go new file mode 100644 index 0000000..0ca1014 --- /dev/null +++ b/css/utilities.go @@ -0,0 +1,1489 @@ +// Code generated from YAML configs. DO NOT EDIT. +package css + +import ( + "fmt" + "sync" + "github.com/computesdk/zforge/css/internal" +) + +type Class string + +func (c Class) String() string { + return string(c) +} + +// Global class tracker +var ( + usedClasses = make(map[string]bool) + classMutex sync.RWMutex +) + +// trackClass registers a class as being used +func trackClass(className string) { + classMutex.Lock() + defer classMutex.Unlock() + usedClasses[className] = true +} + +// GetUsedClasses returns a slice of all tracked classes +func GetUsedClasses() []string { + classMutex.RLock() + defer classMutex.RUnlock() + + classes := make([]string, 0, len(usedClasses)) + for class := range usedClasses { + classes = append(classes, class) + } + return classes +} + +// ResetTracking clears all tracked classes +func ResetTracking() { + classMutex.Lock() + defer classMutex.Unlock() + usedClasses = make(map[string]bool) +} + +// Stylesheet wraps the internal stylesheet type +type Stylesheet struct { + internal interface{ GenerateCSS() string } +} + +// Generate returns the CSS string +func (s *Stylesheet) Generate() string { + if s.internal == nil { + return "" + } + if gen, ok := s.internal.(interface{ GenerateCSS() string }); ok { + return gen.GenerateCSS() + } + return "" +} + +// GenerateUtilities creates CSS rules using the config-driven approach +func GenerateUtilities() *Stylesheet { + return &Stylesheet{internal: internal.GenerateUtilities()} +} + +// GenerateMinimalCSS generates CSS only for tracked classes +func GenerateMinimalCSS() *Stylesheet { + return &Stylesheet{internal: internal.GenerateMinimalCSS(GetUsedClasses())} +} + + +// padding applies padding utility +func P(size int) Class { + className := fmt.Sprintf("p-%d", size) + trackClass(className) + return Class(className) +} + + +// padding-x applies padding-x utility +func Px(size int) Class { + className := fmt.Sprintf("px-%d", size) + trackClass(className) + return Class(className) +} + + +// padding-y applies padding-y utility +func Py(size int) Class { + className := fmt.Sprintf("py-%d", size) + trackClass(className) + return Class(className) +} + + +// padding-top applies padding-top utility +func Pt(size int) Class { + className := fmt.Sprintf("pt-%d", size) + trackClass(className) + return Class(className) +} + + +// padding-right applies padding-right utility +func Pr(size int) Class { + className := fmt.Sprintf("pr-%d", size) + trackClass(className) + return Class(className) +} + + +// padding-bottom applies padding-bottom utility +func Pb(size int) Class { + className := fmt.Sprintf("pb-%d", size) + trackClass(className) + return Class(className) +} + + +// padding-left applies padding-left utility +func Pl(size int) Class { + className := fmt.Sprintf("pl-%d", size) + trackClass(className) + return Class(className) +} + + +// margin applies margin utility +func M(size int) Class { + className := fmt.Sprintf("m-%d", size) + trackClass(className) + return Class(className) +} + + +// margin-x applies margin-x utility +func Mx(size int) Class { + className := fmt.Sprintf("mx-%d", size) + trackClass(className) + return Class(className) +} + + +// margin-y applies margin-y utility +func My(size int) Class { + className := fmt.Sprintf("my-%d", size) + trackClass(className) + return Class(className) +} + + +// margin-top applies margin-top utility +func Mt(size int) Class { + className := fmt.Sprintf("mt-%d", size) + trackClass(className) + return Class(className) +} + + +// margin-right applies margin-right utility +func Mr(size int) Class { + className := fmt.Sprintf("mr-%d", size) + trackClass(className) + return Class(className) +} + + +// margin-bottom applies margin-bottom utility +func Mb(size int) Class { + className := fmt.Sprintf("mb-%d", size) + trackClass(className) + return Class(className) +} + + +// margin-left applies margin-left utility +func Ml(size int) Class { + className := fmt.Sprintf("ml-%d", size) + trackClass(className) + return Class(className) +} + + +// BgGray applies bg-gray-shade utility +func BgGray(shade int) Class { + className := fmt.Sprintf("bg-gray-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextGray applies text-gray-shade utility +func TextGray(shade int) Class { + className := fmt.Sprintf("text-gray-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgGreen applies bg-green-shade utility +func BgGreen(shade int) Class { + className := fmt.Sprintf("bg-green-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextGreen applies text-green-shade utility +func TextGreen(shade int) Class { + className := fmt.Sprintf("text-green-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgFuchsia applies bg-fuchsia-shade utility +func BgFuchsia(shade int) Class { + className := fmt.Sprintf("bg-fuchsia-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextFuchsia applies text-fuchsia-shade utility +func TextFuchsia(shade int) Class { + className := fmt.Sprintf("text-fuchsia-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgZinc applies bg-zinc-shade utility +func BgZinc(shade int) Class { + className := fmt.Sprintf("bg-zinc-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextZinc applies text-zinc-shade utility +func TextZinc(shade int) Class { + className := fmt.Sprintf("text-zinc-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgNeutral applies bg-neutral-shade utility +func BgNeutral(shade int) Class { + className := fmt.Sprintf("bg-neutral-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextNeutral applies text-neutral-shade utility +func TextNeutral(shade int) Class { + className := fmt.Sprintf("text-neutral-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgRed applies bg-red-shade utility +func BgRed(shade int) Class { + className := fmt.Sprintf("bg-red-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextRed applies text-red-shade utility +func TextRed(shade int) Class { + className := fmt.Sprintf("text-red-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgEmerald applies bg-emerald-shade utility +func BgEmerald(shade int) Class { + className := fmt.Sprintf("bg-emerald-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextEmerald applies text-emerald-shade utility +func TextEmerald(shade int) Class { + className := fmt.Sprintf("text-emerald-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgTeal applies bg-teal-shade utility +func BgTeal(shade int) Class { + className := fmt.Sprintf("bg-teal-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextTeal applies text-teal-shade utility +func TextTeal(shade int) Class { + className := fmt.Sprintf("text-teal-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgIndigo applies bg-indigo-shade utility +func BgIndigo(shade int) Class { + className := fmt.Sprintf("bg-indigo-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextIndigo applies text-indigo-shade utility +func TextIndigo(shade int) Class { + className := fmt.Sprintf("text-indigo-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgStone applies bg-stone-shade utility +func BgStone(shade int) Class { + className := fmt.Sprintf("bg-stone-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextStone applies text-stone-shade utility +func TextStone(shade int) Class { + className := fmt.Sprintf("text-stone-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgLime applies bg-lime-shade utility +func BgLime(shade int) Class { + className := fmt.Sprintf("bg-lime-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextLime applies text-lime-shade utility +func TextLime(shade int) Class { + className := fmt.Sprintf("text-lime-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgSky applies bg-sky-shade utility +func BgSky(shade int) Class { + className := fmt.Sprintf("bg-sky-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextSky applies text-sky-shade utility +func TextSky(shade int) Class { + className := fmt.Sprintf("text-sky-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgBlue applies bg-blue-shade utility +func BgBlue(shade int) Class { + className := fmt.Sprintf("bg-blue-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextBlue applies text-blue-shade utility +func TextBlue(shade int) Class { + className := fmt.Sprintf("text-blue-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgViolet applies bg-violet-shade utility +func BgViolet(shade int) Class { + className := fmt.Sprintf("bg-violet-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextViolet applies text-violet-shade utility +func TextViolet(shade int) Class { + className := fmt.Sprintf("text-violet-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgRose applies bg-rose-shade utility +func BgRose(shade int) Class { + className := fmt.Sprintf("bg-rose-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextRose applies text-rose-shade utility +func TextRose(shade int) Class { + className := fmt.Sprintf("text-rose-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgPurple applies bg-purple-shade utility +func BgPurple(shade int) Class { + className := fmt.Sprintf("bg-purple-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextPurple applies text-purple-shade utility +func TextPurple(shade int) Class { + className := fmt.Sprintf("text-purple-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgOrange applies bg-orange-shade utility +func BgOrange(shade int) Class { + className := fmt.Sprintf("bg-orange-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextOrange applies text-orange-shade utility +func TextOrange(shade int) Class { + className := fmt.Sprintf("text-orange-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgAmber applies bg-amber-shade utility +func BgAmber(shade int) Class { + className := fmt.Sprintf("bg-amber-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextAmber applies text-amber-shade utility +func TextAmber(shade int) Class { + className := fmt.Sprintf("text-amber-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgYellow applies bg-yellow-shade utility +func BgYellow(shade int) Class { + className := fmt.Sprintf("bg-yellow-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextYellow applies text-yellow-shade utility +func TextYellow(shade int) Class { + className := fmt.Sprintf("text-yellow-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgCyan applies bg-cyan-shade utility +func BgCyan(shade int) Class { + className := fmt.Sprintf("bg-cyan-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextCyan applies text-cyan-shade utility +func TextCyan(shade int) Class { + className := fmt.Sprintf("text-cyan-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgPink applies bg-pink-shade utility +func BgPink(shade int) Class { + className := fmt.Sprintf("bg-pink-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextPink applies text-pink-shade utility +func TextPink(shade int) Class { + className := fmt.Sprintf("text-pink-%d", shade) + trackClass(className) + return Class(className) +} + + +// BgSlate applies bg-slate-shade utility +func BgSlate(shade int) Class { + className := fmt.Sprintf("bg-slate-%d", shade) + trackClass(className) + return Class(className) +} + + +// TextSlate applies text-slate-shade utility +func TextSlate(shade int) Class { + className := fmt.Sprintf("text-slate-%d", shade) + trackClass(className) + return Class(className) +} + + +// Block applies block utility +func Block() Class { + trackClass("block") + return "block" +} + + +// Flex applies flex utility +func Flex() Class { + trackClass("flex") + return "flex" +} + + +// Grid applies grid utility +func Grid() Class { + trackClass("grid") + return "grid" +} + + +// Hidden applies hidden utility +func Hidden() Class { + trackClass("hidden") + return "hidden" +} + + +// Inline applies inline utility +func Inline() Class { + trackClass("inline") + return "inline" +} + + +// InlineBlock applies inline-block utility +func InlineBlock() Class { + trackClass("inline-block") + return "inline-block" +} + + +// InlineFlex applies inline-flex utility +func InlineFlex() Class { + trackClass("inline-flex") + return "inline-flex" +} + + +// InlineGrid applies inline-grid utility +func InlineGrid() Class { + trackClass("inline-grid") + return "inline-grid" +} + + +// JustifyStart applies justify-start utility +func JustifyStart() Class { + trackClass("justify-start") + return "justify-start" +} + + +// JustifyCenter applies justify-center utility +func JustifyCenter() Class { + trackClass("justify-center") + return "justify-center" +} + + +// JustifyEnd applies justify-end utility +func JustifyEnd() Class { + trackClass("justify-end") + return "justify-end" +} + + +// JustifyBetween applies justify-between utility +func JustifyBetween() Class { + trackClass("justify-between") + return "justify-between" +} + + +// JustifyAround applies justify-around utility +func JustifyAround() Class { + trackClass("justify-around") + return "justify-around" +} + + +// JustifyEvenly applies justify-evenly utility +func JustifyEvenly() Class { + trackClass("justify-evenly") + return "justify-evenly" +} + + +// ItemsStart applies items-start utility +func ItemsStart() Class { + trackClass("items-start") + return "items-start" +} + + +// ItemsCenter applies items-center utility +func ItemsCenter() Class { + trackClass("items-center") + return "items-center" +} + + +// ItemsEnd applies items-end utility +func ItemsEnd() Class { + trackClass("items-end") + return "items-end" +} + + +// ItemsStretch applies items-stretch utility +func ItemsStretch() Class { + trackClass("items-stretch") + return "items-stretch" +} + + +// ItemsBaseline applies items-baseline utility +func ItemsBaseline() Class { + trackClass("items-baseline") + return "items-baseline" +} + + +// FlexRow applies flex-row utility +func FlexRow() Class { + trackClass("flex-row") + return "flex-row" +} + + +// FlexCol applies flex-col utility +func FlexCol() Class { + trackClass("flex-col") + return "flex-col" +} + + +// FlexRowReverse applies flex-row-reverse utility +func FlexRowReverse() Class { + trackClass("flex-row-reverse") + return "flex-row-reverse" +} + + +// FlexColReverse applies flex-col-reverse utility +func FlexColReverse() Class { + trackClass("flex-col-reverse") + return "flex-col-reverse" +} + + +// TextSm applies text-sm utility +func TextSm() Class { + trackClass("text-sm") + return "text-sm" +} + + +// TextLg applies text-lg utility +func TextLg() Class { + trackClass("text-lg") + return "text-lg" +} + + +// TextXl applies text-xl utility +func TextXl() Class { + trackClass("text-xl") + return "text-xl" +} + + +// Text4xl applies text-4xl utility +func Text4xl() Class { + trackClass("text-4xl") + return "text-4xl" +} + + +// Text5xl applies text-5xl utility +func Text5xl() Class { + trackClass("text-5xl") + return "text-5xl" +} + + +// Text8xl applies text-8xl utility +func Text8xl() Class { + trackClass("text-8xl") + return "text-8xl" +} + + +// Text9xl applies text-9xl utility +func Text9xl() Class { + trackClass("text-9xl") + return "text-9xl" +} + + +// TextBase applies text-base utility +func TextBase() Class { + trackClass("text-base") + return "text-base" +} + + +// Text2XL applies text-2xl utility +func Text2XL() Class { + trackClass("text-2xl") + return "text-2xl" +} + + +// Text3XL applies text-3xl utility +func Text3XL() Class { + trackClass("text-3xl") + return "text-3xl" +} + + +// Text6xl applies text-6xl utility +func Text6xl() Class { + trackClass("text-6xl") + return "text-6xl" +} + + +// Text7xl applies text-7xl utility +func Text7xl() Class { + trackClass("text-7xl") + return "text-7xl" +} + + +// TextXs applies text-xs utility +func TextXs() Class { + trackClass("text-xs") + return "text-xs" +} + + +// TextLeft applies text-left utility +func TextLeft() Class { + trackClass("text-left") + return "text-left" +} + + +// TextCenter applies text-center utility +func TextCenter() Class { + trackClass("text-center") + return "text-center" +} + + +// TextRight applies text-right utility +func TextRight() Class { + trackClass("text-right") + return "text-right" +} + + +// TextJustify applies text-justify utility +func TextJustify() Class { + trackClass("text-justify") + return "text-justify" +} + + +// FontThin applies font-thin utility +func FontThin() Class { + trackClass("font-thin") + return "font-thin" +} + + +// FontExtralight applies font-extralight utility +func FontExtralight() Class { + trackClass("font-extralight") + return "font-extralight" +} + + +// FontLight applies font-light utility +func FontLight() Class { + trackClass("font-light") + return "font-light" +} + + +// FontNormal applies font-normal utility +func FontNormal() Class { + trackClass("font-normal") + return "font-normal" +} + + +// FontMedium applies font-medium utility +func FontMedium() Class { + trackClass("font-medium") + return "font-medium" +} + + +// FontSemibold applies font-semibold utility +func FontSemibold() Class { + trackClass("font-semibold") + return "font-semibold" +} + + +// FontBold applies font-bold utility +func FontBold() Class { + trackClass("font-bold") + return "font-bold" +} + + +// FontExtrabold applies font-extrabold utility +func FontExtrabold() Class { + trackClass("font-extrabold") + return "font-extrabold" +} + + +// FontBlack applies font-black utility +func FontBlack() Class { + trackClass("font-black") + return "font-black" +} + + +// FontSans applies font-sans utility +func FontSans() Class { + trackClass("font-sans") + return "font-sans" +} + + +// FontSerif applies font-serif utility +func FontSerif() Class { + trackClass("font-serif") + return "font-serif" +} + + +// FontMono applies font-mono utility +func FontMono() Class { + trackClass("font-mono") + return "font-mono" +} + + +// Border applies border utility +func Border(width int) Class { + className := fmt.Sprintf("border-%d", width) + trackClass(className) + return Class(className) +} + + +// BorderT applies border-t utility +func BorderT(width int) Class { + className := fmt.Sprintf("border-t-%d", width) + trackClass(className) + return Class(className) +} + + +// BorderR applies border-r utility +func BorderR(width int) Class { + className := fmt.Sprintf("border-r-%d", width) + trackClass(className) + return Class(className) +} + + +// BorderB applies border-b utility +func BorderB(width int) Class { + className := fmt.Sprintf("border-b-%d", width) + trackClass(className) + return Class(className) +} + + +// BorderL applies border-l utility +func BorderL(width int) Class { + className := fmt.Sprintf("border-l-%d", width) + trackClass(className) + return Class(className) +} + + +// Rounded applies rounded utility +func Rounded(radius int) Class { + className := fmt.Sprintf("rounded-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedT applies rounded-t utility +func RoundedT(radius int) Class { + className := fmt.Sprintf("rounded-t-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedR applies rounded-r utility +func RoundedR(radius int) Class { + className := fmt.Sprintf("rounded-r-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedB applies rounded-b utility +func RoundedB(radius int) Class { + className := fmt.Sprintf("rounded-b-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedL applies rounded-l utility +func RoundedL(radius int) Class { + className := fmt.Sprintf("rounded-l-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedTl applies rounded-tl utility +func RoundedTl(radius int) Class { + className := fmt.Sprintf("rounded-tl-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedTr applies rounded-tr utility +func RoundedTr(radius int) Class { + className := fmt.Sprintf("rounded-tr-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedBr applies rounded-br utility +func RoundedBr(radius int) Class { + className := fmt.Sprintf("rounded-br-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedBl applies rounded-bl utility +func RoundedBl(radius int) Class { + className := fmt.Sprintf("rounded-bl-%d", radius) + trackClass(className) + return Class(className) +} + + +// RoundedFull applies rounded-full utility +func RoundedFull() Class { + trackClass("rounded-full") + return "rounded-full" +} + + +// RoundedNone applies rounded-none utility +func RoundedNone() Class { + trackClass("rounded-none") + return "rounded-none" +} + + +// W applies width utility +func W(size string) Class { + className := fmt.Sprintf("w-%s", size) + trackClass(className) + return Class(className) +} + + +// H applies height utility +func H(size string) Class { + className := fmt.Sprintf("h-%s", size) + trackClass(className) + return Class(className) +} + + +// MaxW applies max-width utility +func MaxW(size string) Class { + className := fmt.Sprintf("max-w-%s", size) + trackClass(className) + return Class(className) +} + + +// MinW applies min-width utility +func MinW(size string) Class { + className := fmt.Sprintf("min-w-%s", size) + trackClass(className) + return Class(className) +} + + +// MaxH applies max-height utility +func MaxH(size string) Class { + className := fmt.Sprintf("max-h-%s", size) + trackClass(className) + return Class(className) +} + + +// MinH applies min-height utility +func MinH(size string) Class { + className := fmt.Sprintf("min-h-%s", size) + trackClass(className) + return Class(className) +} + + +// Static applies static utility +func Static() Class { + trackClass("static") + return "static" +} + + +// Fixed applies fixed utility +func Fixed() Class { + trackClass("fixed") + return "fixed" +} + + +// Absolute applies absolute utility +func Absolute() Class { + trackClass("absolute") + return "absolute" +} + + +// Relative applies relative utility +func Relative() Class { + trackClass("relative") + return "relative" +} + + +// Sticky applies sticky utility +func Sticky() Class { + trackClass("sticky") + return "sticky" +} + + +// Top applies top position utility +func Top(value string) Class { + className := fmt.Sprintf("top-%s", value) + trackClass(className) + return Class(className) +} + + +// Right applies right position utility +func Right(value string) Class { + className := fmt.Sprintf("right-%s", value) + trackClass(className) + return Class(className) +} + + +// Bottom applies bottom position utility +func Bottom(value string) Class { + className := fmt.Sprintf("bottom-%s", value) + trackClass(className) + return Class(className) +} + + +// Left applies left position utility +func Left(value string) Class { + className := fmt.Sprintf("left-%s", value) + trackClass(className) + return Class(className) +} + + +// Inset applies inset utility +func Inset(value string) Class { + className := fmt.Sprintf("inset-%s", value) + trackClass(className) + return Class(className) +} + + +// InsetX applies horizontal inset utility +func InsetX(value string) Class { + className := fmt.Sprintf("inset-x-%s", value) + trackClass(className) + return Class(className) +} + + +// InsetY applies vertical inset utility +func InsetY(value string) Class { + className := fmt.Sprintf("inset-y-%s", value) + trackClass(className) + return Class(className) +} + + +// Z applies z-index utility +func Z(value string) Class { + className := fmt.Sprintf("z-%s", value) + trackClass(className) + return Class(className) +} + + +// OverflowAuto applies overflow-auto utility +func OverflowAuto() Class { + trackClass("overflow-auto") + return "overflow-auto" +} + + +// OverflowHidden applies overflow-hidden utility +func OverflowHidden() Class { + trackClass("overflow-hidden") + return "overflow-hidden" +} + + +// OverflowVisible applies overflow-visible utility +func OverflowVisible() Class { + trackClass("overflow-visible") + return "overflow-visible" +} + + +// OverflowScroll applies overflow-scroll utility +func OverflowScroll() Class { + trackClass("overflow-scroll") + return "overflow-scroll" +} + + +// OverflowXAuto applies overflow-x-auto utility +func OverflowXAuto() Class { + trackClass("overflow-x-auto") + return "overflow-x-auto" +} + + +// OverflowXHidden applies overflow-x-hidden utility +func OverflowXHidden() Class { + trackClass("overflow-x-hidden") + return "overflow-x-hidden" +} + + +// OverflowXVisible applies overflow-x-visible utility +func OverflowXVisible() Class { + trackClass("overflow-x-visible") + return "overflow-x-visible" +} + + +// OverflowXScroll applies overflow-x-scroll utility +func OverflowXScroll() Class { + trackClass("overflow-x-scroll") + return "overflow-x-scroll" +} + + +// OverflowYAuto applies overflow-y-auto utility +func OverflowYAuto() Class { + trackClass("overflow-y-auto") + return "overflow-y-auto" +} + + +// OverflowYHidden applies overflow-y-hidden utility +func OverflowYHidden() Class { + trackClass("overflow-y-hidden") + return "overflow-y-hidden" +} + + +// OverflowYVisible applies overflow-y-visible utility +func OverflowYVisible() Class { + trackClass("overflow-y-visible") + return "overflow-y-visible" +} + + +// OverflowYScroll applies overflow-y-scroll utility +func OverflowYScroll() Class { + trackClass("overflow-y-scroll") + return "overflow-y-scroll" +} + + +// Opacity applies opacity utility +func Opacity(value int) Class { + className := fmt.Sprintf("opacity-%d", value) + trackClass(className) + return Class(className) +} + + +// Shadow applies shadow utility +func Shadow(size ...string) Class { + var className string + if len(size) > 0 && size[0] != "" { + className = fmt.Sprintf("shadow-%s", size[0]) + } else { + className = "shadow" + } + trackClass(className) + return Class(className) +} + + +// CursorAuto applies cursor-auto utility +func CursorAuto() Class { + trackClass("cursor-auto") + return "cursor-auto" +} + + +// CursorDefault applies cursor-default utility +func CursorDefault() Class { + trackClass("cursor-default") + return "cursor-default" +} + + +// CursorPointer applies cursor-pointer utility +func CursorPointer() Class { + trackClass("cursor-pointer") + return "cursor-pointer" +} + + +// CursorWait applies cursor-wait utility +func CursorWait() Class { + trackClass("cursor-wait") + return "cursor-wait" +} + + +// CursorText applies cursor-text utility +func CursorText() Class { + trackClass("cursor-text") + return "cursor-text" +} + + +// CursorMove applies cursor-move utility +func CursorMove() Class { + trackClass("cursor-move") + return "cursor-move" +} + + +// CursorHelp applies cursor-help utility +func CursorHelp() Class { + trackClass("cursor-help") + return "cursor-help" +} + + +// CursorNotAllowed applies cursor-not-allowed utility +func CursorNotAllowed() Class { + trackClass("cursor-not-allowed") + return "cursor-not-allowed" +} + + +// CursorNone applies cursor-none utility +func CursorNone() Class { + trackClass("cursor-none") + return "cursor-none" +} + + +// CursorContextMenu applies cursor-context-menu utility +func CursorContextMenu() Class { + trackClass("cursor-context-menu") + return "cursor-context-menu" +} + + +// CursorProgress applies cursor-progress utility +func CursorProgress() Class { + trackClass("cursor-progress") + return "cursor-progress" +} + + +// CursorCell applies cursor-cell utility +func CursorCell() Class { + trackClass("cursor-cell") + return "cursor-cell" +} + + +// CursorCrosshair applies cursor-crosshair utility +func CursorCrosshair() Class { + trackClass("cursor-crosshair") + return "cursor-crosshair" +} + + +// CursorVerticalText applies cursor-vertical-text utility +func CursorVerticalText() Class { + trackClass("cursor-vertical-text") + return "cursor-vertical-text" +} + + +// CursorAlias applies cursor-alias utility +func CursorAlias() Class { + trackClass("cursor-alias") + return "cursor-alias" +} + + +// CursorCopy applies cursor-copy utility +func CursorCopy() Class { + trackClass("cursor-copy") + return "cursor-copy" +} + + +// CursorNoDrop applies cursor-no-drop utility +func CursorNoDrop() Class { + trackClass("cursor-no-drop") + return "cursor-no-drop" +} + + +// CursorGrab applies cursor-grab utility +func CursorGrab() Class { + trackClass("cursor-grab") + return "cursor-grab" +} + + +// CursorGrabbing applies cursor-grabbing utility +func CursorGrabbing() Class { + trackClass("cursor-grabbing") + return "cursor-grabbing" +} + + +// SelectNone applies select-none utility +func SelectNone() Class { + trackClass("select-none") + return "select-none" +} + + +// SelectText applies select-text utility +func SelectText() Class { + trackClass("select-text") + return "select-text" +} + + +// SelectAll applies select-all utility +func SelectAll() Class { + trackClass("select-all") + return "select-all" +} + + +// SelectAuto applies select-auto utility +func SelectAuto() Class { + trackClass("select-auto") + return "select-auto" +} + + +// PointerEventsNone applies pointer-events-none utility +func PointerEventsNone() Class { + trackClass("pointer-events-none") + return "pointer-events-none" +} + + +// PointerEventsAuto applies pointer-events-auto utility +func PointerEventsAuto() Class { + trackClass("pointer-events-auto") + return "pointer-events-auto" +} + + +// Visible applies visible utility +func Visible() Class { + trackClass("visible") + return "visible" +} + + +// Invisible applies invisible utility +func Invisible() Class { + trackClass("invisible") + return "invisible" +} + + +// Collapse applies collapse utility +func Collapse() Class { + trackClass("collapse") + return "collapse" +} + + +// SrOnly applies sr-only utility +func SrOnly() Class { + trackClass("sr-only") + return "sr-only" +} + + +// NotSrOnly applies not-sr-only utility +func NotSrOnly() Class { + trackClass("not-sr-only") + return "not-sr-only" +} + diff --git a/css/utilities_test.go b/css/utilities_test.go new file mode 100644 index 0000000..922f518 --- /dev/null +++ b/css/utilities_test.go @@ -0,0 +1,330 @@ +package css_test + +import ( + "strings" + "testing" + + "github.com/computesdk/zforge/css" + "github.com/stretchr/testify/assert" +) + +func TestClassString(t *testing.T) { + class := css.P(4) + assert.Equal(t, "p-4", class.String()) +} + +func TestSpacingUtilities(t *testing.T) { + tests := []struct { + name string + fn func(int) css.Class + value int + expected string + }{ + {"padding", css.P, 4, "p-4"}, + {"padding-x", css.Px, 2, "px-2"}, + {"padding-y", css.Py, 8, "py-8"}, + {"padding-top", css.Pt, 1, "pt-1"}, + {"padding-right", css.Pr, 3, "pr-3"}, + {"padding-bottom", css.Pb, 6, "pb-6"}, + {"padding-left", css.Pl, 0, "pl-0"}, + {"margin", css.M, 4, "m-4"}, + {"margin-x", css.Mx, 2, "mx-2"}, + {"margin-y", css.My, 8, "my-8"}, + {"margin-top", css.Mt, 1, "mt-1"}, + {"margin-right", css.Mr, 3, "mr-3"}, + {"margin-bottom", css.Mb, 6, "mb-6"}, + {"margin-left", css.Ml, 0, "ml-0"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.fn(tt.value) + assert.Equal(t, tt.expected, string(result)) + }) + } +} + +func TestColorUtilities(t *testing.T) { + tests := []struct { + name string + bgFn func(int) css.Class + textFn func(int) css.Class + shade int + bgExp string + textExp string + }{ + {"indigo", css.BgIndigo, css.TextIndigo, 500, "bg-indigo-500", "text-indigo-500"}, + {"purple", css.BgPurple, css.TextPurple, 700, "bg-purple-700", "text-purple-700"}, + {"rose", css.BgRose, css.TextRose, 300, "bg-rose-300", "text-rose-300"}, + {"amber", css.BgAmber, css.TextAmber, 400, "bg-amber-400", "text-amber-400"}, + {"green", css.BgGreen, css.TextGreen, 600, "bg-green-600", "text-green-600"}, + {"blue", css.BgBlue, css.TextBlue, 50, "bg-blue-50", "text-blue-50"}, + {"red", css.BgRed, css.TextRed, 900, "bg-red-900", "text-red-900"}, + {"gray", css.BgGray, css.TextGray, 500, "bg-gray-500", "text-gray-500"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bgResult := tt.bgFn(tt.shade) + assert.Equal(t, tt.bgExp, string(bgResult)) + + textResult := tt.textFn(tt.shade) + assert.Equal(t, tt.textExp, string(textResult)) + }) + } +} + +func TestDisplayUtilities(t *testing.T) { + tests := []struct { + name string + fn func() css.Class + expected string + }{ + {"block", css.Block, "block"}, + {"flex", css.Flex, "flex"}, + {"grid", css.Grid, "grid"}, + {"hidden", css.Hidden, "hidden"}, + {"inline", css.Inline, "inline"}, + {"inline-block", css.InlineBlock, "inline-block"}, + {"inline-flex", css.InlineFlex, "inline-flex"}, + {"inline-grid", css.InlineGrid, "inline-grid"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.fn() + assert.Equal(t, tt.expected, string(result)) + }) + } +} + +func TestFlexUtilities(t *testing.T) { + tests := []struct { + name string + fn func() css.Class + expected string + }{ + {"justify-start", css.JustifyStart, "justify-start"}, + {"justify-center", css.JustifyCenter, "justify-center"}, + {"justify-end", css.JustifyEnd, "justify-end"}, + {"justify-between", css.JustifyBetween, "justify-between"}, + {"justify-around", css.JustifyAround, "justify-around"}, + {"justify-evenly", css.JustifyEvenly, "justify-evenly"}, + {"items-start", css.ItemsStart, "items-start"}, + {"items-center", css.ItemsCenter, "items-center"}, + {"items-end", css.ItemsEnd, "items-end"}, + {"items-stretch", css.ItemsStretch, "items-stretch"}, + {"items-baseline", css.ItemsBaseline, "items-baseline"}, + {"flex-row", css.FlexRow, "flex-row"}, + {"flex-col", css.FlexCol, "flex-col"}, + {"flex-row-reverse", css.FlexRowReverse, "flex-row-reverse"}, + {"flex-col-reverse", css.FlexColReverse, "flex-col-reverse"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.fn() + assert.Equal(t, tt.expected, string(result)) + }) + } +} + +func TestTypographyUtilities(t *testing.T) { + tests := []struct { + name string + fn func() css.Class + expected string + }{ + {"text-xs", css.TextXs, "text-xs"}, + {"text-sm", css.TextSm, "text-sm"}, + {"text-base", css.TextBase, "text-base"}, + {"text-lg", css.TextLg, "text-lg"}, + {"text-xl", css.TextXl, "text-xl"}, + {"text-2xl", css.Text2XL, "text-2xl"}, + {"text-3xl", css.Text3XL, "text-3xl"}, + {"text-left", css.TextLeft, "text-left"}, + {"text-center", css.TextCenter, "text-center"}, + {"text-right", css.TextRight, "text-right"}, + {"text-justify", css.TextJustify, "text-justify"}, + {"font-thin", css.FontThin, "font-thin"}, + {"font-light", css.FontLight, "font-light"}, + {"font-normal", css.FontNormal, "font-normal"}, + {"font-medium", css.FontMedium, "font-medium"}, + {"font-semibold", css.FontSemibold, "font-semibold"}, + {"font-bold", css.FontBold, "font-bold"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.fn() + assert.Equal(t, tt.expected, string(result)) + }) + } +} + +func TestBorderUtilities(t *testing.T) { + tests := []struct { + name string + fn func(int) css.Class + value int + expected string + }{ + {"border", css.Border, 2, "border-2"}, + {"border-t", css.BorderT, 1, "border-t-1"}, + {"border-r", css.BorderR, 4, "border-r-4"}, + {"border-b", css.BorderB, 0, "border-b-0"}, + {"border-l", css.BorderL, 8, "border-l-8"}, + {"rounded", css.Rounded, 4, "rounded-4"}, + {"rounded-t", css.RoundedT, 2, "rounded-t-2"}, + {"rounded-r", css.RoundedR, 6, "rounded-r-6"}, + {"rounded-b", css.RoundedB, 8, "rounded-b-8"}, + {"rounded-l", css.RoundedL, 1, "rounded-l-1"}, + {"rounded-tl", css.RoundedTl, 3, "rounded-tl-3"}, + {"rounded-tr", css.RoundedTr, 5, "rounded-tr-5"}, + {"rounded-br", css.RoundedBr, 7, "rounded-br-7"}, + {"rounded-bl", css.RoundedBl, 9, "rounded-bl-9"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.fn(tt.value) + assert.Equal(t, tt.expected, string(result)) + }) + } + + t.Run("rounded-full", func(t *testing.T) { + assert.Equal(t, "rounded-full", string(css.RoundedFull())) + }) + + t.Run("rounded-none", func(t *testing.T) { + assert.Equal(t, "rounded-none", string(css.RoundedNone())) + }) +} + +func TestGenerateUtilities(t *testing.T) { + stylesheet := css.GenerateUtilities() + assert.NotNil(t, stylesheet) + + cssContent := stylesheet.Generate() + assert.NotEmpty(t, cssContent) + + // Check for some expected CSS rules + expectedRules := []string{ + ".p-4", + ".m-2", + ".flex", + ".block", + ".hidden", + } + + for _, rule := range expectedRules { + assert.True(t, strings.Contains(cssContent, rule), "Expected CSS to contain %s", rule) + } +} + +func TestMultipleClasses(t *testing.T) { + // Test combining multiple classes + classes := []css.Class{ + css.Flex(), + css.ItemsCenter(), + css.JustifyBetween(), + css.P(4), + css.BgBlue(500), + css.TextBlue(100), + } + + var classNames []string + for _, c := range classes { + classNames = append(classNames, c.String()) + } + + result := strings.Join(classNames, " ") + expected := "flex items-center justify-between p-4 bg-blue-500 text-blue-100" + assert.Equal(t, expected, result) +} + +func TestClassTracking(t *testing.T) { + // Reset tracking before test + css.ResetTracking() + + // Initially should have no tracked classes + assert.Empty(t, css.GetUsedClasses()) + + // Use some classes + css.P(4) + css.BgRed(500) + css.Flex() + css.Rounded(8) + + // Should now have tracked classes + usedClasses := css.GetUsedClasses() + assert.Len(t, usedClasses, 4) + + // Check that all expected classes are tracked + expectedClasses := map[string]bool{ + "p-4": true, + "bg-red-500": true, + "flex": true, + "rounded-8": true, + } + + for _, class := range usedClasses { + assert.True(t, expectedClasses[class], "Unexpected class tracked: %s", class) + } +} + +func TestResetTracking(t *testing.T) { + // Use some classes + css.P(2) + css.BgBlue(300) + + // Should have tracked classes + assert.NotEmpty(t, css.GetUsedClasses()) + + // Reset tracking + css.ResetTracking() + + // Should now be empty + assert.Empty(t, css.GetUsedClasses()) +} + +func TestGenerateMinimalCSS(t *testing.T) { + // Reset tracking + css.ResetTracking() + + // Use only a few specific classes + css.P(4) + css.BgGreen(100) + css.TextGreen(800) + css.Rounded(4) + + // Generate minimal CSS + stylesheet := css.GenerateMinimalCSS() + assert.NotNil(t, stylesheet) + + cssContent := stylesheet.Generate() + assert.NotEmpty(t, cssContent) + + // Should contain CSS for the classes we used + expectedRules := []string{ + ".p-4", + ".bg-green-100", + ".text-green-800", + ".rounded-4", + } + + for _, rule := range expectedRules { + assert.True(t, strings.Contains(cssContent, rule), "Expected minimal CSS to contain %s", rule) + } + + // Should NOT contain CSS for classes we didn't use + unexpectedRules := []string{ + ".p-8", // Different padding + ".bg-red-500", // Different color + ".rounded-full", // Different border radius + } + + for _, rule := range unexpectedRules { + assert.False(t, strings.Contains(cssContent, rule), "Expected minimal CSS NOT to contain %s", rule) + } +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2a1ecfc --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module github.com/computesdk/zforge + +go 1.22.5 + +require ( + github.com/stretchr/testify v1.10.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..713a0b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/html/container.go b/html/container.go new file mode 100644 index 0000000..0098273 --- /dev/null +++ b/html/container.go @@ -0,0 +1,43 @@ +package html + +// Div creates a new div element +func Div(children ...*Element) *Element { + elem := &Element{Tag: "div"} + return elem.AddChildren(children...) +} + +// Section creates a new section element +func Section(children ...*Element) *Element { + elem := &Element{Tag: "section"} + return elem.AddChildren(children...) +} + +// Article creates a new article element +func Article(children ...*Element) *Element { + elem := &Element{Tag: "article"} + return elem.AddChildren(children...) +} + +// Header creates a new header element +func Header(children ...*Element) *Element { + elem := &Element{Tag: "header"} + return elem.AddChildren(children...) +} + +// Footer creates a new footer element +func Footer(children ...*Element) *Element { + elem := &Element{Tag: "footer"} + return elem.AddChildren(children...) +} + +// Nav creates a new nav element +func Nav(children ...*Element) *Element { + elem := &Element{Tag: "nav"} + return elem.AddChildren(children...) +} + +// Main creates a new main element +func Main(children ...*Element) *Element { + elem := &Element{Tag: "main"} + return elem.AddChildren(children...) +} \ No newline at end of file diff --git a/html/document.go b/html/document.go new file mode 100644 index 0000000..c4a72c6 --- /dev/null +++ b/html/document.go @@ -0,0 +1,44 @@ +package html + +// Html creates a new html element +func Html(children ...*Element) *Element { + elem := &Element{Tag: "html"} + return elem.AddChildren(children...) +} + +// Head creates a new head element +func Head(children ...*Element) *Element { + elem := &Element{Tag: "head"} + return elem.AddChildren(children...) +} + +// Body creates a new body element +func Body(children ...*Element) *Element { + elem := &Element{Tag: "body"} + return elem.AddChildren(children...) +} + +// Title creates a new title element +func Title(content string) *Element { + return &Element{Tag: "title", Content: content} +} + +// Meta creates a new meta element (self-closing) +func Meta() *Element { + return &Element{Tag: "meta"} +} + +// Link creates a new link element (self-closing) +func Link() *Element { + return &Element{Tag: "link"} +} + +// Script creates a new script element +func Script(content string) *Element { + return &Element{Tag: "script", Content: content} +} + +// Style creates a new style element +func Style(content string) *Element { + return &Element{Tag: "style", Content: content} +} \ No newline at end of file diff --git a/html/element.go b/html/element.go new file mode 100644 index 0000000..2a6045a --- /dev/null +++ b/html/element.go @@ -0,0 +1,150 @@ +package html + +import ( + "fmt" + "slices" + "strings" + + "github.com/computesdk/zforge/css" +) + +type Element struct { + Tag string + Content string + Attributes map[string]string + Children []Element +} + +// New creates a new element with the specified tag +func New(tag string) *Element { + return &Element{Tag: tag} +} + +// Class sets the class attribute and returns the element for chaining +func (e *Element) Class(classes ...css.Class) *Element { + if e.Attributes == nil { + e.Attributes = make(map[string]string) + } + + classStrings := make([]string, len(classes)) + for i, class := range classes { + classStrings[i] = class.String() + } + + e.Attributes["class"] = strings.Join(classStrings, " ") + return e +} + +// ID sets the id attribute and returns the element for chaining +func (e *Element) ID(id string) *Element { + if e.Attributes == nil { + e.Attributes = make(map[string]string) + } + e.Attributes["id"] = id + return e +} + +// Attr sets a custom attribute and returns the element for chaining +func (e *Element) Attr(key, value string) *Element { + if e.Attributes == nil { + e.Attributes = make(map[string]string) + } + e.Attributes[key] = value + return e +} + +// AddChildren adds children to the element and returns the element for chaining +func (e *Element) AddChildren(children ...*Element) *Element { + for _, child := range children { + if child != nil { + e.Children = append(e.Children, *child) + } + } + return e +} + +// SetContent sets the content and returns the element for chaining +func (e *Element) SetContent(content string) *Element { + e.Content = content + return e +} + +// Render processes the element tree and returns the final HTML string +func (e *Element) Render() string { + // Inject minimal CSS if a head element exists and CSS classes were used + head := e.findHead() + if head != nil { + usedClasses := css.GetUsedClasses() + if len(usedClasses) > 0 { + stylesheet := css.GenerateMinimalCSS() + head.Children = append(head.Children, *Style(stylesheet.Generate())) + } + } + + // Generate and return HTML string + return e.toHTML() +} + + +// findHead recursively searches for the first head element in the document tree +func (e *Element) findHead() *Element { + // Check if this element is a head + if e.Tag == "head" { + return e + } + + // Recursively search children + for i := range e.Children { + if found := e.Children[i].findHead(); found != nil { + return found + } + } + + return nil +} + +// toHTML converts the element and its children to an HTML string +func (e *Element) toHTML() string { + if e == nil { + return "" + } + + // Handle text-only elements (no tag) + if e.Tag == "" { + return e.Content + } + + html := fmt.Sprintf("<%s", e.Tag) + + for key, value := range e.Attributes { + html += fmt.Sprintf(` %s="%s"`, key, value) + } + + if isSelfClosing(e.Tag) { + html += " />" + return html + } + + html += ">" + + if e.Content != "" { + html += e.Content + } + + for _, child := range e.Children { + html += child.toHTML() + } + + html += fmt.Sprintf("", e.Tag) + return html +} + +// isSelfClosing checks if an HTML tag is self-closing +func isSelfClosing(tag string) bool { + selfClosingTags := []string{ + "area", "base", "br", "col", "embed", "hr", "img", "input", + "link", "meta", "param", "source", "track", "wbr", + } + + return slices.Contains(selfClosingTags, strings.ToLower(tag)) +} \ No newline at end of file diff --git a/html/element_test.go b/html/element_test.go new file mode 100644 index 0000000..84ea0cf --- /dev/null +++ b/html/element_test.go @@ -0,0 +1,190 @@ +package html_test + +import ( + "testing" + + "github.com/computesdk/zforge/css" + "github.com/computesdk/zforge/html" +) + +func TestBasicRendering(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test simple div with content + div := html.Div().SetContent("Hello World") + result := div.Render() + expected := "
Hello World
" + + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } +} + +func TestWithAttributes(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test div with attributes using fluent API + div := html.Div().SetContent("Styled content").Class(css.P(4), css.BgBlue(100)).ID("main") + result := div.Render() + + // Should contain both attributes (order may vary) + if !contains(result, `class="p-4 bg-blue-100"`) || !contains(result, `id="main"`) { + t.Errorf("Expected attributes not found in: %s", result) + } +} + +func TestSelfClosingTags(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test self-closing img tag + img := html.Img("/path/to/image.jpg").Attr("alt", "Test image") + result := img.Render() + + // Check that it's a self-closing img tag with the correct attributes + if !contains(result, ``) { + t.Errorf("Expected self-closing img tag, got: %s", result) + } + if !contains(result, `src="/path/to/image.jpg"`) || !contains(result, `alt="Test image"`) { + t.Errorf("Expected attributes not found in: %s", result) + } +} + +func TestNestedElements(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test nested structure using fluent API + div := html.Div( + html.H1("Welcome"), + html.P("This is a test paragraph"), + ) + + result := div.Render() + expected := "

Welcome

This is a test paragraph

" + + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } +} + +func TestCompleteDocument(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test a complete HTML document structure using fluent API + document := html.Html( + html.Head( + html.Title("Test Page"), + ), + html.Body( + html.H1("Welcome to Test Page"), + html.P("This is a test paragraph with some content."), + ), + ) + + result := document.Render() + + expected := "Test Page

Welcome to Test Page

This is a test paragraph with some content.

" + + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } +} + +func TestMethodChaining(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test method chaining + div := html.Div(). + Class("container"). + ID("main"). + Attr("data-test", "value"). + AddChildren( + html.H1("Title").Class("header"), + html.P("Content").ID("content"), + ) + + result := div.Render() + + // Check all attributes are present + if !contains(result, `class="container"`) { + t.Errorf("Expected class attribute not found in: %s", result) + } + if !contains(result, `id="main"`) { + t.Errorf("Expected id attribute not found in: %s", result) + } + if !contains(result, `data-test="value"`) { + t.Errorf("Expected data-test attribute not found in: %s", result) + } + if !contains(result, `

Title

`) { + t.Errorf("Expected h1 with class not found in: %s", result) + } + if !contains(result, `

Content

`) { + t.Errorf("Expected p with id not found in: %s", result) + } +} + +func TestCSSIntegration(t *testing.T) { + // Reset CSS tracking for clean test + css.ResetTracking() + + // Test CSS utility integration + div := html.Div(). + SetContent("Styled with CSS utilities"). + Class(css.P(4), css.M(2), css.BgGray(100), css.TextGray(800), css.Rounded(8)) + + result := div.Render() + + expected := `class="p-4 m-2 bg-gray-100 text-gray-800 rounded-8"` + if !contains(result, expected) { + t.Errorf("Expected CSS classes not found. Got: %s", result) + } +} + +func TestRenderWithCSSInjection(t *testing.T) { + // Reset CSS tracking + css.ResetTracking() + + // Create a document with CSS classes + document := html.Html( + html.Head( + html.Title("CSS Test"), + ), + html.Body( + html.Div().SetContent("Test content").Class(css.P(4), css.BgRed(100)), + ), + ) + + result := document.Render() + + // Should contain CSS styles in head + if !contains(result, "