Your collection
+A complete card with native HTML parts.
+ New +diff --git a/liquid/README.md b/liquid/README.md index fa5b58990..5cc3362ac 100644 --- a/liquid/README.md +++ b/liquid/README.md @@ -8,9 +8,9 @@ Native Shopify Liquid, readable CSS and ES modules. The consumer needs no Tailwi `registry.json` is the install contract. Every item is universal, every file has an explicit `~/assets/` or `~/snippets/` target, and dependencies use full Liquid registry URLs. Individual items install their prerequisites, including shared popup styles and runtime where needed. `components` is the complete set. `init` is tokens and base styles only. -CSS and Liquid are maintained by hand. The shared neutral palette comes from `registry/styles/shadcn/themes.json`; `styles/primitives.css` holds native sizes, radii, shadows and media roles. Theme colors, settings, content, Shopify forms and layouts remain theme-owned. A consumer loads its scheme overrides after `ui`, and `custom.css` after `content_for_header`. +Only shared neutral palette tokens, the part inventory, licenses and file hashes are generated by `scripts/registry-liquid.mjs`. CSS and Liquid are maintained by hand. The palette comes from `registry/styles/shadcn/themes.json`; `styles/primitives.css` holds native sizes, radii, shadows and media roles. Theme colors, settings, content, Shopify forms and layouts remain theme-owned. A consumer loads its scheme overrides after `ui`, and `custom.css` after `content_for_header`. -Registry generation and installation tests are added separately from this source layer. Local LiquidJS fixtures do not verify a store's commerce objects or Shopify's stylesheet subsetting. +Run `pnpm registry:liquid` after a source change. Commit `public/r/liquid`, `contracts.json`, `styles/tokens.css`, `licenses.json` and `manifest.json`. `pnpm test:liquid` verifies the real CLI, the dependency closures, token parity and repeat installs. `pnpm test:browser` verifies a CLI-installed copy in Chromium, Firefox and WebKit, including native forms and editor lifecycle. The local renderer is LiquidJS with explicit Shopify filter substitutes. It does not verify a store's commerce objects or Shopify's stylesheet subsetting. This first port follows Vega and the complete reusable storefront layer from `fulldotdev/liquid-template` at `e13fc25`. Sidebar, Resizable, Menubar, Scroll Area and React-only app components are outside this native storefront set. Shopify blocks, commerce adapters, translation keys and settings are not installed by the UI registry. The existing storefront media and card variants are additive to the shared component contract. Other Tailwind presets are not translated automatically. diff --git a/liquid/examples/accordion.liquid b/liquid/examples/accordion.liquid new file mode 100644 index 000000000..c5305c1c1 --- /dev/null +++ b/liquid/examples/accordion.liquid @@ -0,0 +1,10 @@ +
Place your product details here.
Add care instructions for your product.
This clears the choices in the form above.
+A complete card with native HTML parts.
+ New +Collapsible content stays visible without JavaScript.
+Keyboard and focus test.
+Swipe down or press Escape to close.
+Sort by
+ + + + + + + +Try a different search.
+A hover card previews a link on hover or keyboard focus.
+Item title
+Add a short description of this item.
+Popover
+A popover holds short, nonessential content and links.
+Add a short introduction.
+Collection details
+Care instructions
diff --git a/liquid/examples/sheet.liquid b/liquid/examples/sheet.liquid new file mode 100644 index 000000000..c7a7a26c1 --- /dev/null +++ b/liquid/examples/sheet.liquid @@ -0,0 +1,11 @@ +Keyboard and focus test.
+| Item | +Quantity | +Price | +
|---|---|---|
| Example 1 | +1 | +1000 | +
| Example 2 | +2 | +2000 | +
| Example 3 | +3 | +3000 | +
| Total | +14000 | +|
Add the product description.
+Add materials, dimensions or other details.
+Add care instructions.
+Add your details here.
+Add care instructions here.
diff --git a/liquid/examples/toggle-group.liquid b/liquid/examples/toggle-group.liquid new file mode 100644 index 000000000..b4001a7d7 --- /dev/null +++ b/liquid/examples/toggle-group.liquid @@ -0,0 +1,50 @@ +Readable text uses the same semantic tokens as the rest of your theme.
+Add your own content here.+
s&&(s=i*p)}s>d&&(d=s),u=n.indexOf(l,u+1)}return o[s]=d,d}function b(e){return e.toLowerCase().replace(v,` `)}function ne(e,t,n=[]){let r=n.length>0?`${e} ${n.join(` `)}`:e;return y(r,t,b(r),b(t),0,0,{})}const x=Symbol(`data-slot.command.authored-value`),S=Symbol(`data-slot.command.inferred-value`),C=e=>e==null?null:e.trim(),re=e=>e?e.split(/[,\\n]/).map(e=>e.trim()).filter(Boolean):[],ie=(e,t)=>Array.from(e.children).filter(e=>e instanceof HTMLElement&&e.getAttribute(`data-slot`)===t),ae=e=>{let t=e=>e.nodeType===Node.TEXT_NODE?e.textContent??``:!(e instanceof HTMLElement)||e.getAttribute(`data-slot`)===`command-shortcut`?``:Array.from(e.childNodes,t).join(``);return t(e).replace(/\\s+/g,` `).trim()},oe=e=>e.hasAttribute(`disabled`)||e.hasAttribute(`data-disabled`)||e.getAttribute(`aria-disabled`)===`true`,se=(e,t)=>{let n=e,r=e.hasAttribute(`data-value`);if(n[x]===void 0&&(n[x]=r),r?n[x]||(n[x]=C(e.getAttribute(`data-value`))!==n[S]):n[x]=!1,n[x])return{authored:!0,value:C(e.getAttribute(`data-value`))};let i=t();return n[S]=i,i===null?e.removeAttribute(`data-value`):e.setAttribute(`data-value`,i),{authored:!1,value:i}},w=`@data-slot/command`,T=`[data-slot=\"command-item\"]`,E=`[data-slot=\"command-group\"]`,ce=`[data-slot=\"command-separator\"]`,le=[`input:not([type=\"hidden\"])`,`textarea`,`select`,`button`,`a[href]`,`summary`,`audio[controls]`,`video[controls]`,`[contenteditable=\"\"]`,`[contenteditable=\"true\"]`,`[contenteditable=\"plaintext-only\"]`,`[tabindex]:not([tabindex=\"-1\"])`].join(`, `),ue=(e,t,n)=>t.trim()?ne(e,t,n??[]):1,de=(e,t)=>{let n=e;for(;n&&n.parentElement&&n.parentElement!==t;)n=n.parentElement;return n?.parentElement===t?n:null},D=e=>Array.from(e.children).filter(e=>e instanceof HTMLElement);function O(c,l={}){let p=ee(c,w,`[@data-slot/command] createCommand() called more than once for the same root. Returning the existing controller. Destroy it before rebinding with new options.`);if(p)return p;let m=c,h=s(c,`command-input`),g=s(c,`command-list`);if(!h||!g)throw Error(`Command requires command-input and command-list slots`);let _=o(c,g,`[data-slot=\"command-empty\"]`)[0]??null,v=l.label??a(m,`label`)??`Command Menu`,y=l.shouldFilter??i(m,`shouldFilter`)??!0,b=l.loop??i(m,`loop`)??!1,ne=l.disablePointerSelection??i(m,`disablePointerSelection`)??!1,x=l.vimBindings??i(m,`vimBindings`)??!0,S=l.filter??ue,O=l.onValueChange,fe=l.onSearchChange,pe=l.onSelect,k=C(l.defaultValue??a(m,`defaultValue`)),A=l.defaultSearch??a(m,`defaultSearch`)??``,j=[],M=[],N=[],P=new Map,F=new Map,I=0,L=[],R=null,z=null,B=!1,V=!1,me=!1,H=[],U=new Map,W=!1,he=n(h,`command-input`),ge=n(g,`command-list`);m.hasAttribute(`tabindex`)||(m.tabIndex=-1,me=!0),h.setAttribute(`role`,`combobox`),h.setAttribute(`aria-autocomplete`,`list`),h.setAttribute(`aria-expanded`,`true`),h.setAttribute(`aria-controls`,ge),h.setAttribute(`autocomplete`,`off`),h.setAttribute(`autocorrect`,`off`),h.spellcheck=!1,g.setAttribute(`role`,`listbox`),g.tabIndex=-1;let _e=document.querySelector(`label[for=\"${CSS.escape(he)}\"]`);if(_e){let e=n(_e,`command-label`),t=h.getAttribute(`aria-labelledby`);h.setAttribute(`aria-labelledby`,t?`${t} ${e}`:e),g.setAttribute(`aria-labelledby`,e)}else !h.hasAttribute(`aria-label`)&&!h.hasAttribute(`aria-labelledby`)&&(h.setAttribute(`aria-label`,v),g.setAttribute(`aria-label`,`Suggestions`));let ve=()=>{document.activeElement!==h&&h.focus({preventScroll:!0})},ye=e=>{if(!(e instanceof HTMLElement)||!m.contains(e))return null;let t=e.closest(le);return t instanceof HTMLElement&&m.contains(t)?t:null},be=e=>{R?.disconnect();try{return e()}finally{He()}},xe=()=>{g.style.setProperty(`--command-list-height`,`${g.scrollHeight.toFixed(1)}px`)},Se=(e,t)=>{let n=e.filter(e=>t.includes(e));for(let e of t){if(n.includes(e))continue;let r=t.indexOf(e),i=!1;for(let a=r-1;a>=0;--a){let r=t[a];if(!r)continue;let o=n.indexOf(r);if(o!==-1){n.splice(o+1,0,e),i=!0;break}}if(!i){for(let a=r+1;a Parts stay native HTML. Keyboard and focus test.
+ {{ 'component_reference.intro' | t }}
+
+ {{ 'component_reference.heading' | t: level: 1 }}
+
+ {{ 'component_reference.heading' | t: level: 2 }}
+
+ {{ 'component_reference.heading' | t: level: 3 }}
+
+ {{ 'component_reference.heading' | t: level: 4 }}
+
+ {{ 'component_reference.lead' | t }}
+
+ {{ 'component_reference.paragraph' | t }}
+
+ {{- 'component_reference.link' | t -}}
+
+
+ {{ 'component_reference.large' | t }}
+
+ {{ 'component_reference.small' | t }}
+
+ {{ 'component_reference.muted' | t }}
+
+ {{ 'component_reference.rich_text_text' | t }}
+
+ {{ 'component_reference.rich_inline_html' | t }}
+
+ {{- 'component_reference.link' | t -}}
+
+ {{ 'component_reference.list_item' | t: number: 1 }} {{ 'component_reference.rich_nested' | t }} {{ 'component_reference.quote' | t }} {{ 'component_reference.rich_paragraph' | t }} {{ 'component_reference.rich_opt_out' | t }} {{ key | t }} {{ 'component_reference.rich_paragraph' | t }} {{ 'component_reference.rich_paragraph' | t }}
+ {{ 'component_reference.list_item' | t: number: 1 }}
+ {{ 'component_reference.rich_nested' | t }}
+ {{ 'component_reference.rich_paragraph' | t }}
+
+ {{- 'component_reference.link' | t -}}
+
+ {{ 'component_reference.rich_opt_out' | t }}
+ {{ 'component_reference.forms_text' | t }}
+
+ {{ 'component_reference.item_title' | t }}
+
+ {{ 'component_reference.item_text' | t }}
+ {{ key | t }}
+ {{ 'component_reference.more_text' | t }}
+
+ {{ 'component_reference.overlays_text' | t }}
+
+ {{ 'component_reference.popover' | t }}
+
+ {{ 'component_reference.popover_text' | t }}
+
+ {{ 'component_reference.hover_card_text' | t }}
+
+ {{ 'component_reference.sort' | t }}
+
+ {{ 'component_reference.drawer_text' | t }}
+ Split headline
+ Centered headline
+ Section headline
+ removed text, an SKU, a note1, Esc and code.",
+ "rich_nested": "A list item can hold its own paragraphs and a nested list.",
+ "rich_paragraph": "Running text follows the size of its rich text block and reads slightly larger on phones.",
+ "rich_opt_out": "This part keeps the theme's own styles through data-not-typeset.",
+ "rich_accordion": "Rich text in an accordion",
+ "forms": "Forms",
+ "actions": "Actions",
+ "content": "Content",
+ "overlays": "Overlays",
+ "heading": "Heading {{ level }}",
+ "lead": "Lead text introduces a page or section.",
+ "paragraph": "Body text reads at 16 pixels with a relaxed line height.",
+ "link": "A text link",
+ "quote": "A blockquote sets a quotation apart from the text around it.",
+ "list_item": "List item {{ number }}",
+ "large": "Large text",
+ "small": "Small text",
+ "muted": "Muted text adds secondary detail.",
+ "role": "Role",
+ "size": "Size",
+ "forms_text": "Every control works without JavaScript through its native fallback, and submits its value with the form.",
+ "choose": "Choose a size",
+ "delivery": "Delivery",
+ "standard": "Standard",
+ "express": "Express",
+ "pickup": "Pickup",
+ "gift_wrap": "Gift wrap",
+ "updates": "Email me updates",
+ "range": "Range",
+ "minimum": "Minimum",
+ "maximum": "Maximum",
+ "submit": "Submit",
+ "reset": "Reset",
+ "reset_title": "Reset the form?",
+ "reset_text": "This clears the choices in the form above.",
+ "cancel": "Cancel",
+ "shortcuts": "Shortcuts",
+ "go_to": "Go to",
+ "products": "Products",
+ "favorite": "Favorite",
+ "view": "View",
+ "grid": "Grid",
+ "list": "List",
+ "loading": "Loading",
+ "please_wait": "Please wait",
+ "item_title": "Item title",
+ "item_text": "Add a short description of this item.",
+ "view_products": "View products",
+ "table_caption": "Example data",
+ "item": "Item",
+ "example": "Example {{ number }}",
+ "total": "Total",
+ "file": "example-file.pdf",
+ "uploading": "Uploading",
+ "failed": "Upload failed",
+ "product_details": "Product details",
+ "description": "Description",
+ "details": "Details",
+ "care": "Care",
+ "description_text": "Add the product description.",
+ "details_text": "Add materials, dimensions or other details.",
+ "care_text": "Add care instructions.",
+ "more": "Show more",
+ "more_text": "Collapsible content stays visible without JavaScript.",
+ "overlays_text": "Popups open from a trigger that shows once its controller has started. Keep essential information on the page itself.",
+ "popover": "Popover",
+ "popover_text": "A popover holds short, nonessential content and links.",
+ "hover_card_text": "A hover card previews a link on hover or keyboard focus.",
+ "menu": "Menu",
+ "sort": "Sort by",
+ "featured": "Featured",
+ "newest": "Newest",
+ "in_stock": "In stock only",
+ "drawer": "Drawer",
+ "drawer_text": "Swipe down or press Escape to close.",
+ "toast": "Show toast",
+ "toast_title": "Toast",
+ "toast_text": "A toast confirms an action and closes by itself.",
+ "notifications": "Notifications",
+ "size_small": "Small",
+ "size_medium": "Medium",
+ "size_large": "Large"
+ },
+ "search": { "title": "Search" },
+ "cart": { "title": "Cart" },
+ "products": { "quantity": "Quantity", "price": "Price" }
+}
diff --git a/tests/fixtures/liquid/icon.liquid b/tests/fixtures/liquid/icon.liquid
new file mode 100644
index 000000000..1adca8715
--- /dev/null
+++ b/tests/fixtures/liquid/icon.liquid
@@ -0,0 +1,145 @@
+{% doc %}
+ Renders a decorative interface icon. Paths are derived from Lucide; see LICENSE-LUCIDE.md.
+
+ @param {string} name - Icon name
+ @param {string} [class] - Extra class names
+{% enddoc %}
+
diff --git a/tests/fixtures/liquid/interactive.liquid b/tests/fixtures/liquid/interactive.liquid
new file mode 100644
index 000000000..71a3da137
--- /dev/null
+++ b/tests/fixtures/liquid/interactive.liquid
@@ -0,0 +1,97 @@
+Native Liquid card
+ Example dialog
+ {{ 'component_reference.title' | t }}
+
+ {{ 'component_reference.typography' | t }}
+
+ data-slot
+
+ {{ 'component_reference.quote' | t }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'component_reference.role' | t }}
+
+
+ {{ 'component_reference.size' | t }}
+
+
+
+
+ {{ 'component_reference.lead' | t }}
+
+ 18px
+
+
+
+ {{ 'component_reference.paragraph' | t }}
+
+ 16px
+
+
+
+
+ {{ 'component_reference.muted' | t }}
+
+ 14px
+
+ {{ 'component_reference.rich_text' | t }}
+
+ {{ 'component_reference.heading' | t: level: 2 }}
+ {{ 'component_reference.heading' | t: level: 3 }}
+
+
+
+
+
+
+
+
+
+
+ {{ 'component_reference.heading' | t: level: 4 }}
+
+
+
+
+
+
+ {{ 'component_reference.role' | t }}
+ {{ 'component_reference.size' | t }}
+
+
+
+ {{ 'component_reference.heading' | t: level: 2 }}
+
+ 1.25em
+
+
+
+ {{ 'component_reference.heading' | t: level: 3 }}
+
+ 1.125em
+
+
+
+
+ {{ 'component_reference.heading' | t: level: 5 }}
+
+ 0.875em
+ {{ 'component_reference.heading' | t: level: 5 }}
+ {{ 'component_reference.heading' | t: level: 6 }}
+
+
+
+
+
+ {{ 'component_reference.heading' | t: level: 3 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'component_reference.forms' | t }}
+
+
+ {{ 'component_reference.actions' | t }}
+
+
+ {{ 'component_reference.content' | t }}
+
+
+
+
+
+
+
+
+
+
+ {% for number in (1..3) %}
+
+ {{ 'component_reference.item' | t }}
+
+
+ {{ 'products.quantity' | t }}
+
+
+ {{ 'products.price' | t }}
+
+
+
+ {% endfor %}
+
+
+
+ {{ 'component_reference.example' | t: number: number }}
+
+ {{ number }}
+
+ {{ number | times: 1000 | money }}
+
+
+
+
+
+ {{ 'component_reference.total' | t }}
+
+
+ {{ 14000 | money }}
+
+
+ {{ 'component_reference.overlays' | t }}
+
+
+ {{ 'component_reference.drawer' | t }}
+
+