Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/native-liquid-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"fulldev-ui": minor
---

Add a native Liquid registry for Shopify themes with handwritten component styles, shared neutral tokens, data-slot ES modules and editor lifecycle. Universal shadcn items install directly into theme assets and snippets without Tailwind or a consuming build step.

Integrate Liquid as the third docs framework, with its own navigation, 58 component pages, native rendered previews, usage and installation examples, Markdown endpoints and AI indexes.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Fulldev UI

A shadcn-compatible set of components built for [Astro][astro], designed for content-driven websites.
A shadcn-compatible UI library for Astro, React and native Shopify Liquid.

## Features

Expand All @@ -12,6 +12,10 @@ A shadcn-compatible set of components built for [Astro][astro], designed for con
- **TypeScript** — Full TypeScript support
- **React extension**: the Fulldev extras and blocks for official shadcn/ui React projects; see [react/README.md](react/README.md)

## Native Liquid

Install into a Shopify theme with `vp dlx shadcn@latest add https://ui.full.dev/r/liquid/components.json`, then render `ui` once in the layout head. Readable CSS, native ES modules and shared tokens, with no theme build step. See [Liquid](https://ui.full.dev/liquid/) and [liquid/README.md](liquid/README.md).

## Installation

### Prerequisites
Expand Down
2 changes: 2 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
// Read by the html-validate CLI in the build.
"html-validate.config.mjs",
"liquid/runtime/*.js",
// src/lib/liquid.ts loads this in the generated preview HTML.
"public/preview/liquid.js",
],
// Read as text by src/lib/styles.ts and the registry scripts.
"ignoreFiles": [
Expand Down
4 changes: 4 additions & 0 deletions public/_redirects
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,7 @@
/docs/layouts/ /components/layout/ 301!
/docs/mcp/ /docs/introduction/ 301!
/docs/skills/ /docs/introduction/ 301!

# Native Liquid is a platform alongside Astro and React.
/docs/liquid/ /liquid/docs/installation/ 301!
/docs/liquid.md /liquid/docs/installation.md 301!
34 changes: 34 additions & 0 deletions public/preview/liquid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Docs-only bridge. The component controllers come from the native library.
const report = () =>
parent.postMessage(
{ type: "fulldev:liquid-height", height: document.body.scrollHeight },
location.origin
)
new ResizeObserver(report).observe(document.querySelector("#example"))
window.addEventListener("load", report)
window.addEventListener("message", ({ source, origin, data }) => {
if (
source !== parent ||
origin !== location.origin ||
data?.type !== "fulldev:liquid-theme"
)
return
document.documentElement.classList.toggle("dark", data.dark)
document.documentElement.style.colorScheme = data.dark ? "dark" : "light"
document.querySelector("#preview-theme").textContent = data.css
for (const href of data.fonts ?? []) {
if (
[...document.querySelectorAll("link[data-font]")].some(
(link) => link.href === href
)
)
continue
const link = document.createElement("link")
link.rel = "stylesheet"
link.href = href
link.dataset.font = ""
document.head.append(link)
}
report()
})
parent.postMessage({ type: "fulldev:liquid-ready" }, location.origin)
4 changes: 4 additions & 0 deletions skills/fulldev-ui/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ components. Start at [llms.txt](https://ui.full.dev/llms.txt), or
[react/llms.txt](https://ui.full.dev/react/llms.txt) for React. Where this skill
and the shadcn skill differ, this skill wins.

## Native Liquid

For Shopify themes, use `https://ui.full.dev/r/liquid/<name>.json` with `shadcn add`; `components` installs the full set. Read [Liquid](https://ui.full.dev/liquid/docs/installation.md). These are universal items with explicit theme targets. Do not run `shadcn init` or add Tailwind to the theme. Render `ui` once in each layout head before theme overrides and other modules. Keep `content_for_header`; load `custom.css` after it. Existing Shopify adapters use `runtime: false` and the exported lifecycle with their own import map. Installed UI stays unchanged; theme settings and commerce code stay local. Native source supports Vega, not the Tailwind preset generator.

## Astro setup

- Fulldev UI serves only the Base UI styles (`base-vega`, `base-nova` and so
Expand Down
23 changes: 22 additions & 1 deletion src/components/create.astro
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const command = (template: string) =>
>
<NativeSelectOption value="astro">Astro</NativeSelectOption>
<NativeSelectOption value="react">React</NativeSelectOption>
<NativeSelectOption value="liquid">Liquid</NativeSelectOption>
</NativeSelect>
</Field>
<FieldGroup class="grid grid-cols-2 gap-3 lg:flex lg:flex-col">
Expand Down Expand Up @@ -193,7 +194,22 @@ const command = (template: string) =>
</StylePreview>
</div>

<Tabs defaultValue="new" class="gap-3">
<div
data-framework-panel="liquid"
hidden
class="flex flex-col gap-3 text-sm"
>
<p>
Install in your Shopify theme and render <code>ui</code> once in the
layout head. Liquid uses native Vega CSS; Tailwind style presets do not
transform it. Customize the shared tokens in your theme.
</p>
<pre class="bg-muted overflow-x-auto rounded-lg p-3 font-mono text-xs"><code>npx shadcn@latest add https://ui.full.dev/r/liquid/components.json</code></pre>
<a href="/liquid/docs/installation/" class="underline">
Liquid installation and theming
</a>
</div>
<Tabs defaultValue="new" class="gap-3" data-framework-exclude="liquid">
<TabsList>
<TabsTrigger value="new">New project</TabsTrigger>
<TabsTrigger value="existing">Existing Fulldev UI project</TabsTrigger>
Expand Down Expand Up @@ -248,6 +264,11 @@ const command = (template: string) =>
"[data-create-framework]"
)
const show = (framework: string) => {
document
.querySelectorAll<HTMLElement>("[data-framework-exclude]")
.forEach((panel) => {
panel.hidden = panel.dataset.frameworkExclude === framework
})
document
.querySelectorAll<HTMLElement>("[data-framework-panel]")
.forEach((panel) => {
Expand Down
9 changes: 8 additions & 1 deletion src/components/docs-header.astro
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ import { ThemeToggle } from "@/components/ui/theme-toggle"

type Props = {
logo: { label: string; href: string; alt?: string }
navigation: { label: string; href: string; reactHref?: string }[]
navigation: {
label: string
href: string
reactHref?: string
liquidHref?: string
}[]
button: { label: string; href: string }
githubRepo: string
githubStars?: string
Expand Down Expand Up @@ -71,6 +76,7 @@ const isCurrent = (href: string) =>
variant="ghost"
size="sm"
data-react-href={item.reactHref}
data-liquid-href={item.liquidHref}
>
{item.label}
</Button>
Expand Down Expand Up @@ -128,6 +134,7 @@ const isCurrent = (href: string) =>
<a
href={item.href}
data-react-href={item.reactHref}
data-liquid-href={item.liquidHref}
aria-current={isCurrent(item.href) ? "page" : undefined}
class="text-foreground aria-[current=page]:text-primary -mx-2 rounded-md px-2 py-2 text-xl font-medium"
>
Expand Down
23 changes: 18 additions & 5 deletions src/components/docs-switcher.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ArrowUpRightIcon from "@lucide/astro/icons/arrow-up-right"
import CheckIcon from "@lucide/astro/icons/check"
import ChevronsUpDownIcon from "@lucide/astro/icons/chevrons-up-down"
import LiquidLogo from "@lucide/astro/icons/droplet"
import AstroLogo from "simple-icons/icons/astro.svg"
import ReactLogo from "simple-icons/icons/react.svg"

Expand Down Expand Up @@ -34,7 +35,7 @@ type Props = {
}

const { current, hrefs, labels } = Astro.props
const logos = { astro: AstroLogo, react: ReactLogo }
const logos = { astro: AstroLogo, react: ReactLogo, liquid: LiquidLogo }
const Logo = logos[current]
const title = (value: string) => value[0].toUpperCase() + value.slice(1)
const [fallback] = NAMED_PRESETS
Expand All @@ -46,7 +47,11 @@ const presets = NAMED_PRESETS.map(({ id, label, config }) => ({
}))
---

<div data-docs-switcher data-presets={JSON.stringify(presets)}>
<div
data-docs-switcher
data-framework={current}
data-presets={JSON.stringify(presets)}
>
<DropdownMenu>
<DropdownMenuTrigger
class={cn(
Expand All @@ -55,7 +60,11 @@ const presets = NAMED_PRESETS.map(({ id, label, config }) => ({
)}
>
<span class="bg-muted grid size-8 shrink-0 place-items-center rounded-md">
<Logo class="size-4" fill="currentColor" aria-hidden="true" />
<Logo
class="size-4"
fill={current === "liquid" ? "none" : "currentColor"}
aria-hidden="true"
/>
</span>
<span class="flex min-w-0 flex-1 flex-col">
<span class="truncate text-sm font-medium">
Expand All @@ -80,7 +89,11 @@ const presets = NAMED_PRESETS.map(({ id, label, config }) => ({
href={hrefs[framework]}
data-framework-choice={framework}
>
<ItemLogo class="size-4" fill="currentColor" aria-hidden="true" />
<ItemLogo
class="size-4"
fill={framework === "liquid" ? "none" : "currentColor"}
aria-hidden="true"
/>
{frameworkLabels[framework]}
{framework === current && <CheckIcon class="ml-auto size-4" />}
</DropdownMenuItem>
Expand Down Expand Up @@ -160,7 +173,7 @@ const presets = NAMED_PRESETS.map(({ id, label, config }) => ({
element.textContent = match?.label ?? customLabel
})
document.querySelectorAll("[data-preset-detail]").forEach((element) => {
element.textContent = `${title(config.style)}, ${title(config.theme)}`
element.textContent = `${element.closest("[data-docs-switcher]")?.getAttribute("data-framework") === "liquid" ? "Vega" : title(config.style)}, ${title(config.theme)}`
})
document
.querySelectorAll<HTMLElement>("[data-preset-check]")
Expand Down
57 changes: 55 additions & 2 deletions src/components/example.astro
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,25 @@ const minHeight = /^header-\d+$/.test(name) ? 640 : undefined
<LiveCode
componentProps={{
name: example.item,
frame: framework === "react",
frame: framework !== "astro",
installCommand:
framework === "liquid"
? `npx shadcn@latest add ${(example.dependencies ?? [example.item]).map((name) => `https://ui.full.dev/r/liquid/${name}.json`).join(" ")}`
: undefined,
codeLabel: example.kind === "examples" ? "Code" : "Usage",
}}
>
{Component && fullPage ? (
{framework === "liquid" ? (
<iframe
slot="example"
data-liquid-preview
src={`/preview/liquid/${example.route}.html`}
title={`${name} native Liquid preview`}
loading="lazy"
class="block w-full"
style="height: 360px"
/>
) : Component && fullPage ? (
<div
slot="example"
class="relative h-[640px] w-full transform-gpu overflow-hidden text-left [&_[data-slot=sidebar-container]]:h-full [&_[data-slot=sidebar-wrapper]]:min-h-full"
Expand Down Expand Up @@ -76,6 +90,7 @@ const minHeight = /^header-\d+$/.test(name) ? 640 : undefined

<script>
import { PRESET_EVENT, readPreset } from "@/lib/preset-store"
import { DEFAULT_CONFIG, presetCss, presetFontUrls } from "@/lib/presets"

// Show each React preview in the docs style. Development serves only vega.
const sync = () => {
Expand All @@ -92,6 +107,44 @@ const minHeight = /^header-\d+$/.test(name) ? 640 : undefined
sync()
document.addEventListener(PRESET_EVENT, sync)

const syncLiquid = () => {
const preset = readPreset()
for (const frame of document.querySelectorAll<HTMLIFrameElement>(
"iframe[data-liquid-preview]"
)) {
frame.contentWindow?.postMessage(
{
type: "fulldev:liquid-theme",
dark: document.documentElement.classList.contains("dark"),
css: preset?.css ?? presetCss(DEFAULT_CONFIG),
fonts: preset?.fonts ?? presetFontUrls(DEFAULT_CONFIG),
},
location.origin
)
}
}
syncLiquid()
document.addEventListener(PRESET_EVENT, syncLiquid)
new MutationObserver(syncLiquid).observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
})
window.addEventListener("message", (event) => {
if (event.origin !== location.origin) return
const frame = [
...document.querySelectorAll<HTMLIFrameElement>(
"iframe[data-liquid-preview]"
),
].find((frame) => frame.contentWindow === event.source)
if (!frame) return
if (event.data?.type === "fulldev:liquid-ready") syncLiquid()
if (
event.data?.type === "fulldev:liquid-height" &&
Number.isFinite(event.data.height)
)
frame.style.height = `${Math.min(Math.max(Math.ceil(event.data.height), 240), 2000)}px`
})

// The preview app reports its height.
window.addEventListener("message", (event) => {
if (event.origin !== location.origin) return
Expand Down
16 changes: 15 additions & 1 deletion src/components/framework-logo.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { HTMLAttributes } from "astro/types"
import { cn } from "@/lib/utils"

type Props = HTMLAttributes<"svg"> & {
name: "astro" | "react"
name: "astro" | "react" | "liquid"
}

const { name, class: className, ...props } = Astro.props
Expand Down Expand Up @@ -43,6 +43,20 @@ const gradient = `astro-flame-${crypto.randomUUID()}`
</linearGradient>
</defs>
</svg>
) : name === "liquid" ? (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class={cn("shrink-0", className)}
aria-hidden="true"
{...props}
>
<path d="M12 2c-3 4-7 7.5-7 12a7 7 0 0 0 14 0c0-4.5-4-8-7-12Z" />
</svg>
) : (
<svg
viewBox="-11.5 -10.23 23 20.46"
Expand Down
4 changes: 4 additions & 0 deletions src/components/home-hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type Props = HomeSchema["hero"] & {
counts: { blocks: number; components: number }
// The same pages in React by href, for the visitor who chose React last.
reactHrefs: Record<string, string | undefined>
liquidHrefs: Record<string, string | undefined>
}

const {
Expand All @@ -25,6 +26,7 @@ const {
presets,
counts,
reactHrefs,
liquidHrefs,
} = Astro.props

// The title in parts, with each framework name as its own part.
Expand Down Expand Up @@ -114,6 +116,7 @@ const sample = {
as="a"
href={button.href}
data-react-href={reactHrefs[button.href]}
data-liquid-href={liquidHrefs[button.href]}
size="lg"
variant={index === 0 ? "default" : "outline"}
>
Expand Down Expand Up @@ -150,6 +153,7 @@ const sample = {
<a
href={components.href}
data-react-href={reactHrefs[components.href]}
data-liquid-href={liquidHrefs[components.href]}
class="font-medium after:absolute after:inset-0"
>
{counts.components} {components.label}
Expand Down
Loading
Loading