diff --git a/.changeset/loud-bushes-try.md b/.changeset/loud-bushes-try.md new file mode 100644 index 0000000..2acb13c --- /dev/null +++ b/.changeset/loud-bushes-try.md @@ -0,0 +1,5 @@ +--- +"tsdot": patch +--- + +feat: added minify so the bundle size is smaller. diff --git a/.changeset/yummy-turtles-search.md b/.changeset/yummy-turtles-search.md new file mode 100644 index 0000000..b232df7 --- /dev/null +++ b/.changeset/yummy-turtles-search.md @@ -0,0 +1,5 @@ +--- +"tsdot": patch +--- + +docs: better docs and examples. diff --git a/README.md b/README.md index 229fd5c..c7700ed 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,10 @@ const result = compiledTemplate({ name: "World" }); > I think each of these need examples. -```sh +``` {{ }} - evaluation {{= }} - interpolation -{{! }} - interpolation with encoding +{{! }} - interpolation with encoding # Does not work it seems. {{# }} - compile-time evaluation/includes and partials {{## #}} - compile-time defines {{? }} - conditionals diff --git a/bun.lockb b/bun.lockb index 4649eae..dd937a6 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/dev/assets/icons/index.ts b/dev/assets/icons/index.ts new file mode 100644 index 0000000..e68d441 --- /dev/null +++ b/dev/assets/icons/index.ts @@ -0,0 +1,4 @@ +export { default as IconCopy } from "./lets-icons_copy-light.svg"; +export { default as IconMoon } from "./line-md_moon-rising-twotone-alt-loop.svg"; +export { default as IconSun } from "./line-md_sun-rising-twotone-loop.svg"; +export { default as IconCheck } from "./material-symbols_check.svg"; diff --git a/dev/assets/icons/lets-icons_copy-light.svg b/dev/assets/icons/lets-icons_copy-light.svg new file mode 100644 index 0000000..2b4c337 --- /dev/null +++ b/dev/assets/icons/lets-icons_copy-light.svg @@ -0,0 +1 @@ + diff --git a/dev/assets/icons/line-md_moon-rising-twotone-alt-loop.svg b/dev/assets/icons/line-md_moon-rising-twotone-alt-loop.svg new file mode 100644 index 0000000..806f039 --- /dev/null +++ b/dev/assets/icons/line-md_moon-rising-twotone-alt-loop.svg @@ -0,0 +1 @@ + diff --git a/dev/assets/icons/line-md_sun-rising-twotone-loop.svg b/dev/assets/icons/line-md_sun-rising-twotone-loop.svg new file mode 100644 index 0000000..815abff --- /dev/null +++ b/dev/assets/icons/line-md_sun-rising-twotone-loop.svg @@ -0,0 +1 @@ + diff --git a/dev/assets/icons/material-symbols_check.svg b/dev/assets/icons/material-symbols_check.svg new file mode 100644 index 0000000..fe35e74 --- /dev/null +++ b/dev/assets/icons/material-symbols_check.svg @@ -0,0 +1 @@ + diff --git a/dev/components/theme-switcher.tsx b/dev/components/theme-switcher.tsx new file mode 100644 index 0000000..fee39e1 --- /dev/null +++ b/dev/components/theme-switcher.tsx @@ -0,0 +1,18 @@ +import { IconMoon, IconSun } from "@/assets/icons"; +import { useThemeContext } from "@/contexts/theme.context"; +import { Show } from "solid-js"; +import { Button } from "./ui/button"; + +export function ThemeSwitcher() { + const { theme, toggleTheme } = useThemeContext(); + + return ( +
+ +
+ ); +} diff --git a/dev/components/ui/button.tsx b/dev/components/ui/button.tsx new file mode 100644 index 0000000..cbf8902 --- /dev/null +++ b/dev/components/ui/button.tsx @@ -0,0 +1,53 @@ +import type { JSX, ValidComponent } from "solid-js"; +import { splitProps } from "solid-js"; + +import * as ButtonPrimitive from "@kobalte/core/button"; +import type { PolymorphicProps } from "@kobalte/core/polymorphic"; +import type { VariantProps } from "class-variance-authority"; +import { cva } from "class-variance-authority"; + +import { cn } from "@/utils/cn"; + +const buttonVariants = cva( + "active:scale-95 transition inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input hover:bg-muted text-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 px-3 text-xs", + lg: "h-11 px-8", + icon: "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +type ButtonProps = ButtonPrimitive.ButtonRootProps & + VariantProps & { class?: string | undefined; children?: JSX.Element }; + +const Button = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as ButtonProps, ["variant", "size", "class"]); + return ( + + ); +}; + +export { Button, buttonVariants }; +export type { ButtonProps }; diff --git a/dev/components/ui/card.tsx b/dev/components/ui/card.tsx new file mode 100644 index 0000000..f39874c --- /dev/null +++ b/dev/components/ui/card.tsx @@ -0,0 +1,43 @@ +import type { Component, ComponentProps } from "solid-js"; +import { splitProps } from "solid-js"; + +import { cn } from "@/utils/cn"; + +const Card: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return ( +
+ ); +}; + +const CardHeader: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return
; +}; + +const CardTitle: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return ( +

+ ); +}; + +const CardDescription: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return

; +}; + +const CardContent: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return

; +}; + +const CardFooter: Component> = (props) => { + const [local, others] = splitProps(props, ["class"]); + return
; +}; + +export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; diff --git a/dev/components/ui/select.tsx b/dev/components/ui/select.tsx new file mode 100644 index 0000000..e44fa2d --- /dev/null +++ b/dev/components/ui/select.tsx @@ -0,0 +1,226 @@ +import type { ComponentProps, JSX, ValidComponent } from "solid-js"; +import { Show, splitProps } from "solid-js"; + +import type { PolymorphicProps } from "@kobalte/core/polymorphic"; +import * as SelectPrimitive from "@kobalte/core/select"; +import { cva } from "class-variance-authority"; + +import { cn } from "@/utils/cn"; + +const Select = SelectPrimitive.Root; +const SelectValue = SelectPrimitive.Value; +const SelectHiddenSelect = SelectPrimitive.HiddenSelect; + +type SelectTriggerProps = + SelectPrimitive.SelectTriggerProps & { + class?: string | undefined; + children?: JSX.Element; + }; +const SelectTrigger = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectTriggerProps, ["class", "children"]); + return ( + + {local.children} + + + + + + ); +}; + +type SelectContentProps = + SelectPrimitive.SelectContentProps & { class?: string | undefined }; + +const SelectContent = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectContentProps, ["class"]); + return ( + + + + + + ); +}; + +type SelectItemProps = SelectPrimitive.SelectItemProps & { + class?: string | undefined; + children?: JSX.Element; +}; + +const SelectItem = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectItemProps, ["class", "children"]); + return ( + + + + + + + + {local.children} + + ); +}; + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", + { + variants: { + variant: { + label: "data-[invalid]:text-destructive", + description: "font-normal text-muted-foreground", + error: "text-xs text-destructive", + }, + }, + defaultVariants: { + variant: "label", + }, + } +); + +type SelectLabelProps = SelectPrimitive.SelectLabelProps & { + class?: string | undefined; +}; + +const SelectLabel = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectLabelProps, ["class"]); + return ; +}; + +type SelectDescriptionProps = + SelectPrimitive.SelectDescriptionProps & { + class?: string | undefined; + }; + +const SelectDescription = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectDescriptionProps, ["class"]); + return ( + + ); +}; + +type SelectErrorMessageProps = + SelectPrimitive.SelectErrorMessageProps & { + class?: string | undefined; + }; + +const SelectErrorMessage = ( + props: PolymorphicProps> +) => { + const [local, others] = splitProps(props as SelectErrorMessageProps, ["class"]); + return ( + + ); +}; + +export { + Select, + SelectContent, + SelectDescription, + SelectErrorMessage, + SelectHiddenSelect, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +}; + +// --- + +export function SelectComp( + rawProps: ComponentProps> & { + renderItem?: (item: T) => JSX.Element; + description?: JSX.Element; + } +) { + const [local, rest] = splitProps(rawProps, ["children", "renderItem"]); + + return ( + + ); +} diff --git a/dev/contexts/theme.context.tsx b/dev/contexts/theme.context.tsx new file mode 100644 index 0000000..4e29d12 --- /dev/null +++ b/dev/contexts/theme.context.tsx @@ -0,0 +1,93 @@ +import { useLocalStorage } from "bagon-hooks"; +import { + createContext, + createEffect, + createSignal, + FlowComponent, + Setter, + useContext, + type Accessor, +} from "solid-js"; + +// =========================================================================== +// Context +// =========================================================================== + +export const themes = ["light", "dark", "system"] as const; + +export type Theme = (typeof themes)[number]; + +export type ThemeContextValue = { + theme: Accessor; + setTheme: Setter; + inferredTheme: Accessor>; + toggleTheme: () => void; +}; + +const ThemeContext = createContext({ + theme: () => "light", + setTheme: () => {}, + inferredTheme: () => "light", + toggleTheme: () => {}, +} as ThemeContextValue); + +// =========================================================================== +// Hook +// =========================================================================== +export const useThemeContext = () => useContext(ThemeContext); + +// =========================================================================== +// Provider +// =========================================================================== +export const ThemeContextProvider: FlowComponent = (props) => { + const [theme, setTheme] = useLocalStorage({ + key: "quarta-theme", + defaultValue: "system", + }); + + /** For logic that relies on literally just `light` or `dark` themes (i.e. CodeMirror). Also infers system. */ + const [inferredTheme, setInferredTheme] = + createSignal>("light"); + + createEffect(() => { + let themeValue = theme(); + + if (themeValue === "system") { + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + themeValue = prefersDark ? "dark" : "light"; + } + + themes.forEach((themeName) => { + if (themeValue === themeName) { + document.documentElement.classList.add(themeName); + } else { + document.documentElement.classList.remove(themeName); + } + }); + + setInferredTheme(themeValue); + }); + + function toggleTheme() { + if (theme() === "light") { + setTheme("dark"); + } else if (theme() === "dark") { + setTheme("light"); + } else { + setTheme("dark"); + } + } + + return ( + + {props.children} + + ); +}; diff --git a/dev/pages/+Layout.tsx b/dev/pages/+Layout.tsx index d644227..c0b5c3e 100644 --- a/dev/pages/+Layout.tsx +++ b/dev/pages/+Layout.tsx @@ -1,9 +1,10 @@ import { type FlowProps } from "solid-js"; import { useMetadata } from "vike-metadata-solid"; -import { Head } from "vike-solid/Head"; -import { usePageContext } from "vike-solid/usePageContext"; import getTitle from "../utils/get-title"; +import { ThemeSwitcher } from "@/components/theme-switcher"; +import { ThemeContextProvider } from "@/contexts/theme.context"; +import { getRoute } from "@/route-tree.gen"; import "@/styles/app.css"; useMetadata.setGlobalDefaults({ @@ -16,25 +17,43 @@ useMetadata.setGlobalDefaults({ }); export default function RootLayout(props: FlowProps) { - const pageContext = usePageContext(); - return ( <> - - - - - + +
+
+ +
+ +
{props.children}
-
- - {props.children} -
+
+
+

© 2024 tsdot & doT.js. All rights reserved.

+
+
+
+
); } diff --git a/dev/pages/demo/+Page.tsx b/dev/pages/demo/+Page.tsx index 2836479..cd87999 100644 --- a/dev/pages/demo/+Page.tsx +++ b/dev/pages/demo/+Page.tsx @@ -1,85 +1,172 @@ -import { onMount, VoidProps } from "solid-js"; +import { createSignal, onMount, VoidProps } from "solid-js"; import { createStore } from "solid-js/store"; import { useMetadata } from "vike-metadata-solid"; import getTitle from "../../utils/get-title"; -import { useDebouncedCallback } from "../../hooks/use-debounced-callback"; - -// import doT from "@/../src/index"; +import { Button } from "@/components/ui/button"; +import { SelectComp } from "@/components/ui/select"; // Corrected import path +import { cn } from "@/utils/cn"; // Assuming @/utils/cn maps to your utility functions import doT from "../../../src/index"; +import { demos } from "./demos"; // Import Demo type and demos here -type TemplateData = { - name: string; - messages: number; -}; -const INITIAL_TEMPLATE = ` -

Hello, {{=it.name}}!

-

You have {{=it.messages}} new messages.

-`; +// Use the first demo as the initial state +const INITIAL_DEMO = demos[0]; +// INITIAL_TEMPLATE and INITIAL_DATA_STRING are no longer directly used after onMount export default function DemoPage() { useMetadata({ title: getTitle("Demo"), }); + const [selectedDemoValue, setSelectedValueDemo] = createSignal<{ + value: string; + label: string; + } | null>(null); + const [values, setValues] = createStore({ template: "", data: "", result: "", + error: "", // To store error messages during compilation or parsing + selectedDemoId: INITIAL_DEMO.id, // New state to track selected demo }); - let templateFunction: (data: TemplateData) => string = () => "No result"; + // Type for the data passed to doT.template + // This type is just a generic example, the actual data structure will vary by demo + type TemplateData = Record; - function setTemplate(value: string) { - setValues("template", value); - compileTemplate(value); - } + function handleCompile() { + setValues("error", ""); // Clear previous errors - const compileTemplate = useDebouncedCallback((template: string) => { - templateFunction = doT.template(template); - }, 200); - - function setData(value: string) { - setValues("data", value); try { - compileData(JSON.parse(value) ?? ""); - } catch (e) { - console.log("invalid json."); + // 1. Compile the template + const templateFunction = doT.template(values.template); + + // 2. Parse the data + let parsedData: TemplateData; + try { + parsedData = JSON.parse(values.data); + } catch (e) { + setValues("error", "Error parsing data JSON. Please ensure it's valid JSON."); + setValues("result", ""); // Clear result on data parse error + return; + } + + // 3. Render the template with data + const renderedResult = templateFunction(parsedData); + setValues("result", renderedResult); + } catch (_e: any) { + // Catch compilation errors from doT.template + setValues("error", `Error compiling template: ${_e.message || _e}`); + setValues("result", ""); // Clear result on template compile error } } - const compileData = useDebouncedCallback((value: Object) => { - console.log("compiled data"); - setResult(templateFunction(value as TemplateData)); - }, 300); - - function setResult(value: string) { - setValues("result", value); + // New handler for when a demo is selected from the dropdown + function handleDemoChange(params: { label: string; value: string } | null) { + console.log(params, "asd"); + if (!params) return; + console.log(params, "asd2"); + const selected = demos.find((d) => d.id === params.value); + + if (selected) { + setSelectedValueDemo(params); + setValues("selectedDemoId", params.value); + setValues("template", selected.template); + setValues("data", selected.data); + setValues("error", ""); // Clear any existing error when changing demo + // Trigger compilation and render for the new demo + handleCompile(); + } } onMount(() => { - setTemplate(INITIAL_TEMPLATE); - setData(`{ name: "John", messages: 5 }`); + // Set initial values from the first demo + setValues("template", INITIAL_DEMO.template); + setValues("data", INITIAL_DEMO.data); + setValues("selectedDemoId", INITIAL_DEMO.id); + // Trigger initial compilation and render on mount + handleCompile(); }); return ( -
-

Demo

-
- setTemplate(_)} label="Template" /> -
- setData(_)} label="Data" /> - setResult(_)} label="Result" /> +
+

TSDot Demo

+
+ {/* Demo Selector */} +
+
+ + d.label} + optionValue={(d) => d.value} + options={demos.map((d) => ({ value: d.id, label: d.name }))} // Map demos to their IDs as strings for the options + placeholder="Select a demo" + // The class for the trigger can be passed directly to SelectComp + // as it will apply to the underlying SelectTrigger component. + class="w-[280px]" + /> +
+
+ Description: + + {demos.find((d) => d.id === values.selectedDemoId)?.description} + +
- + setValues("template", val)} + label="Template" + /> +
+ setValues("data", val)} + label="Data (JSON)" + error={values.error} + /> + +
+ + {values.error && ( +
{values.error}
+ )} + + + +
+

Live HTML Output:

+ {/* Using innerHTML to display the rendered HTML is intentional for this demo to show results. + In a production app, sanitize input or use a safer method if displaying user-generated HTML. */} +
+
); @@ -87,23 +174,44 @@ export default function DemoPage() { type CodeFieldProps = { value: string; - onInput: (value: string) => void; + onInput?: (value: string) => void; // Made onInput optional label?: string; + readonly?: boolean; + error?: string; }; function CodeField(props: VoidProps) { return ( -
+
+
+ {props.label} +