diff --git a/packages/paleui/src/ui/all.ts b/packages/paleui/src/ui/all.ts index bb1f48d..41488b0 100644 --- a/packages/paleui/src/ui/all.ts +++ b/packages/paleui/src/ui/all.ts @@ -7,6 +7,7 @@ import { schema as badgeSchema } from "./badge"; import { schema as buttonSchema } from "./button"; import { schema as cardSchema } from "./card"; import { schema as mainSchema } from "./main"; +import { schema as tabsSchema } from "./tabs"; import { schema as typographySchema } from "./typography"; function toSchemas(schema: T | readonly T[]) { @@ -21,4 +22,5 @@ export const schema = [ ...toSchemas(buttonSchema), ...toSchemas(cardSchema), ...toSchemas(typographySchema), + ...toSchemas(tabsSchema), ]; diff --git a/packages/paleui/src/ui/tabs.ts b/packages/paleui/src/ui/tabs.ts new file mode 100644 index 0000000..3843a10 --- /dev/null +++ b/packages/paleui/src/ui/tabs.ts @@ -0,0 +1,205 @@ +import { + defineAnatomy, + defineDimensions, + defineExamples, + defineMeta, + defineSchema, + defineStyles, +} from "../shared/types"; +import { dedent } from "../shared/utils"; + +const meta = defineMeta({ + title: "Tabs", + subtitle: "Contained surface for related content and actions.", + description: [ + "Cards use the semantic <article> element.", + "Header, footer, media, and grouped headings are optional.", + ], + tags: [ + { + title: "MDN:
", + url: "https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/article", + }, + ], +}); + +const anatomy = defineAnatomy({ + root: { + selector: "[data-tabs]" as const, + name: "Tabs", + description: ["A card built on <article>."], + children: { + label: { + selector: "label", + name: "Header", + description: ["Label element."], + type: "element", + direct: true, + optional: false, + children: { + input: { + selector: "input", + name: "Input", + description: ["Input element"], + type: "element", + direct: true, + optional: false, + states: { + checked: { + name: "Checked", + selector: ":checked + span + article", + }, + }, + }, + span: { + selector: "span[tab-title]", + name: "Tab Title", + description: ["Tab title element"], + type: "element", + direct: true, + optional: false, + }, + article: { + selector: "article", + name: "Tab Content", + description: ["Tab content element"], + type: "element", + direct: true, + optional: false, + }, + }, + }, + }, + example: dedent(` +
+ + + +
+ `), + }, +}); + +const styles = defineStyles(anatomy, { + root: { + base: ["display: flex", "flex-wrap: wrap", "gap: 0.25rem;"], + }, + label: { + base: ["display: contents"], + }, + input: { + base: ["display: none"], + states: { + checked: ["display: block"], + }, + }, + span: { + base: [ + "padding: 0.75rem 1rem", + "background: #ddd", + "cursor: pointer", + "border-radius: 0.5rem 0.5rem 0 0", + "order: 0", + ], + }, + article: { + base: [ + "display: none", + "width: 100%", + "padding: 1rem", + "border: 1px solid #ddd", + "background: #fff", + "order: 1", + ], + }, +}); + +const dimensions = defineDimensions(anatomy, { + content: { + meta: { + title: "Content", + description: [ + "Cards can be used as a default card, a media card, or a compact card.", + ], + }, + options: { + default: { name: "Default" }, + media: { name: "Media" }, + compact: { name: "Compact" }, + }, + }, +}); + +const examples = defineExamples(dimensions, anatomy, (_keys) => { + const content: Record<(typeof _keys.content)[number], string> = { + default: dedent(` +
+
+
+

Pro plan

+

For growing teams shipping frequently.

+
+
+

Includes deploy previews, team roles, and production analytics.

+
+ + +
+
+ `), + media: dedent(` +
+ Dashboard preview +
+
+

Analytics overview

+

Daily active users, deploy health, and error rates in one place.

+
+
+

Use cards with media when you need a visual preview above the supporting copy.

+
+ `), + compact: dedent(` +
+
+

Seats remaining

+ 12 of 20 assigned +
+

Add more seats before your next billing cycle closes.

+
+ `), + }; + + return { + content, + }; +}); + +const card = { + anatomy, + styles, + dimensions, + examples, +} as const; + +export const schema = defineSchema({ + meta, + components: { + card, + }, +}); diff --git a/packages/site/src/components/ThemePlayground.astro b/packages/site/src/components/ThemePlayground.astro index 8ac5bd6..63752ea 100644 --- a/packages/site/src/components/ThemePlayground.astro +++ b/packages/site/src/components/ThemePlayground.astro @@ -47,11 +47,13 @@ const sampleTheme = `:root {
-

Paste a theme and check it against real component surfaces.

+

+ Paste a theme and check it against real component surfaces. +

- Drop in a standard shadcn variable block with :root and - an optional .dark section. The preview stays scoped to this - page section, so you can validate tokens without changing the rest of the site. + Drop in a standard shadcn variable block with :root and an optional + .dark section. The preview stays scoped to this page section, so + you can validate tokens without changing the rest of the site.

@@ -61,7 +63,8 @@ const sampleTheme = `:root {

Theme Input

- Paste raw CSS variables. The preview honors light tokens and optional dark tokens. + Paste raw CSS variables. The preview honors light tokens and + optional dark tokens.

@@ -77,8 +80,12 @@ const sampleTheme = `:root {
- - + +
@@ -112,15 +119,16 @@ const sampleTheme = `:root {

Theme release review

- Check the same token set across actions, supporting copy, state chips, and denser - surfaces before you publish. + Check the same token set across actions, supporting copy, state + chips, and denser surfaces before you publish.

- This preview is built like a small product workflow, so buttons and badges appear where - they naturally would in a UI rather than as isolated examples. + This preview is built like a small product workflow, so buttons and + badges appear where they naturally would in a UI rather than as + isolated examples.

@@ -136,7 +144,8 @@ const sampleTheme = `:root {

Page review queue

- Use queued surfaces to judge how badges, counters, and utility actions sit together. + Use queued surfaces to judge how badges, counters, and utility + actions sit together.

@@ -151,7 +160,8 @@ const sampleTheme = `:root {

Landing page refresh

- Checks how primary actions and neutral surfaces sit together in a calmer layout. + Checks how primary actions and neutral surfaces sit together + in a calmer layout.

@@ -170,7 +180,8 @@ const sampleTheme = `:root {

Docs shell review

- Good for checking secondary emphasis, quiet borders, and contrast on long-running pages. + Good for checking secondary emphasis, quiet borders, and + contrast on long-running pages.

@@ -189,7 +200,8 @@ const sampleTheme = `:root {

Alert state fallback

- Stress-tests destructive accents and whether warning states still read clearly. + Stress-tests destructive accents and whether warning states + still read clearly.

@@ -212,12 +224,14 @@ const sampleTheme = `:root {

- Headings, body copy, code, and quiet metadata should all remain comfortable once the - theme is doing more than rendering swatches. + Headings, body copy, code, and quiet metadata should all remain + comfortable once the theme is doing more than rendering swatches.

- Use this area to confirm your theme still reads well when a page mixes - primary information, supporting text, and inline code such as + Use this area to confirm your theme still reads well when a page + mixes + primary information, supporting text, and inline + code such as --radius or --ring.

@@ -239,7 +253,12 @@ const sampleTheme = `:root {
- +

Theme review tip

- Toggle the preview into dark mode to confirm your .dark tokens hold up - on alerts, borders, and muted text. + Toggle the preview into dark mode to confirm your .dark tokens hold up on alerts, borders, and muted text.
@@ -256,22 +276,22 @@ const sampleTheme = `:root {
What should I look at first?
- Start with primary buttons, muted copy, and border contrast. Those three usually reveal - token problems quickly. + Start with primary buttons, muted copy, and border contrast. + Those three usually reveal token problems quickly.
How does dark mode work here?
- The preview container gets a dark class, so the same pasted variables can - be checked in both light and dark contexts. + The preview container gets a dark class, so the same + pasted variables can be checked in both light and dark contexts.
Does this affect the rest of the site?
- No. The pasted CSS is rewritten to target this preview section only, leaving the global - nav and other pages alone. + No. The pasted CSS is rewritten to target this preview section + only, leaving the global nav and other pages alone.
@@ -332,7 +352,9 @@ const sampleTheme = `:root { const declarations = new Map(); for (const block of extractBlocks(source, "@theme(?:\\s+inline)?")) { - for (const match of block.matchAll(/(--radius(?:-[a-z0-9]+)?)\s*:\s*([^;]+);/gi)) { + for (const match of block.matchAll( + /(--radius(?:-[a-z0-9]+)?)\s*:\s*([^;]+);/gi, + )) { const [, token, value] = match; declarations.set(token, ` ${token}: ${value.trim()};`); } @@ -346,9 +368,14 @@ const sampleTheme = `:root { if (!trimmed || !/--radius\s*:/.test(trimmed)) return trimmed; const declared = new Set( - Array.from(trimmed.matchAll(/(--radius(?:-[a-z0-9]+)?)\s*:/gi), ([, token]) => token), + Array.from( + trimmed.matchAll(/(--radius(?:-[a-z0-9]+)?)\s*:/gi), + ([, token]) => token, + ), + ); + const missing = derivedRadiusTokens.filter( + ([token]) => !declared.has(token), ); - const missing = derivedRadiusTokens.filter(([token]) => !declared.has(token)); if (!missing.length) return trimmed; const extras = missing @@ -389,7 +416,9 @@ const sampleTheme = `:root { function updateStatus(hasCustomTheme) { if (!statusBadge) return; - statusBadge.textContent = hasCustomTheme ? "Custom theme applied" : "Using site theme"; + statusBadge.textContent = hasCustomTheme + ? "Custom theme applied" + : "Using site theme"; } function applyTheme(source) { @@ -402,7 +431,9 @@ const sampleTheme = `:root { if (!preview || !modeButton) return; const isDark = mode === "dark"; preview.classList.toggle("dark", isDark); - modeButton.textContent = isDark ? "Preview Light Theme" : "Preview Dark Theme"; + modeButton.textContent = isDark + ? "Preview Light Theme" + : "Preview Dark Theme"; localStorage.setItem(MODE_KEY, isDark ? "dark" : "light"); } @@ -428,7 +459,9 @@ const sampleTheme = `:root { textarea.value = savedTheme; applyTheme(savedTheme); - setPreviewMode(savedMode === "dark" || savedMode === "light" ? savedMode : defaultMode); + setPreviewMode( + savedMode === "dark" || savedMode === "light" ? savedMode : defaultMode, + ); applyButton.addEventListener("click", () => { saveAndApply(textarea.value); diff --git a/packages/site/src/pages/index.astro b/packages/site/src/pages/index.astro index f0304e8..9f69381 100644 --- a/packages/site/src/pages/index.astro +++ b/packages/site/src/pages/index.astro @@ -8,17 +8,24 @@ import ThemePlayground from "@/components/ThemePlayground.astro";
-

Semantic UI primitives with a familiar theme model.

+

+ Semantic UI primitives with a familiar theme model. +

- PaleUI gives you plain HTML components styled with CSS variables, so shadcn-style - themes work without bringing along a JS component runtime. + PaleUI gives you plain HTML components styled with CSS variables, so + shadcn-style themes work without bringing along a JS component + runtime.

@@ -28,7 +35,7 @@ import ThemePlayground from "@/components/ThemePlayground.astro";