@tetherto/dev-websites-core 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # `@tetherto/dev-websites-core`
2
2
 
3
- Shared CMS, SEO, i18n, utils, and UI primitives for WDK / QVAC websites.
3
+ Shared CMS, SEO, i18n, utils, UI, and analytics primitives for WDK / QVAC / PEAR / MDK websites.
4
4
 
5
5
  | Import | Contents |
6
6
  | --- | --- |
@@ -8,6 +8,7 @@ Shared CMS, SEO, i18n, utils, and UI primitives for WDK / QVAC websites.
8
8
  | `@tetherto/dev-websites-core/seo` | `createMetadataFactory`, `getSiteUrl`, `getOgBackgroundDataUrl` |
9
9
  | `@tetherto/dev-websites-core/i18n` | `Languages` / `LanguageLabel` |
10
10
  | `@tetherto/dev-websites-core/utils` | `createLogger`, `toPlainValue`, `stripShikiPreBackground` |
11
+ | `@tetherto/dev-websites-core/analytics` | `GtmLoader`, `createTrackedLink` (optional `@next/third-parties` peer) |
11
12
  | `@tetherto/dev-websites-core/ui` | UI primitives (`Button`, `Dialog`, `Pagination`, `Tooltip`, …) + `cn` |
12
13
  | `@tetherto/dev-websites-core/ui/a11y` | `focusRingClassName`, `VisuallyHidden` |
13
14
  | `@tetherto/dev-websites-core/ui/carousel` | `Carousel` (+ optional `embla-carousel-react` peer) |
@@ -50,6 +51,24 @@ rm -rf node_modules/@tetherto/dev-websites-core && npm install @tetherto/dev-web
50
51
  Do **not** import `@tetherto/dev-websites-core/cms` from Client Components — it includes Node-only
51
52
  modules (`fs`, Payload, S3). Prefer domain subpaths over the root barrel.
52
53
 
54
+ ### Analytics
55
+
56
+ ```ts
57
+ import { GtmLoader, createTrackedLink } from '@tetherto/dev-websites-core/analytics'
58
+ import { Link } from '@/i18n/navigation'
59
+ import { analytics } from '@/lib/analytics'
60
+
61
+ // Delay GTM until window.load (optional `@next/third-parties` peer)
62
+ <GtmLoader gtmId={gtmId} />
63
+
64
+ // App owns Link + event catalog; core owns click wiring
65
+ export const TrackedLink = createTrackedLink(Link, {
66
+ docsClicked: analytics.docsClicked,
67
+ getStartedClicked: analytics.getStartedClicked,
68
+ externalLinkClicked: analytics.externalLinkClicked,
69
+ })
70
+ ```
71
+
53
72
  ### UI theming
54
73
 
55
74
  One **Tailwind theme contract** for all sites (same utility names). Defaults are
@@ -87,6 +106,12 @@ Apps define the CSS variables the preset references (`--color-bg-base`,
87
106
  `globals.css`. Product-only utilities (e.g. QVAC `text-heading`) stay in the
88
107
  app config until migrated onto the shared names.
89
108
 
109
+ **Button / form theming (optional):** `solid` fill reads `--btn-primary-bg`
110
+ (fallback: accent); label stays `text-accent-foreground` so per-site accent
111
+ foreground overrides (e.g. QVAC on-action) keep working. `outline` border
112
+ reads `--btn-secondary-border` (fallback: accent). Input/Textarea borders
113
+ read `--control-border` (fallback: `--color-border-subtle`).
114
+
90
115
  Optional CSS helpers: `ui/theme/tokens.css`, `ui/theme/preset.css`, `ui/theme.css`
91
116
  (short-name contract / Tailwind v4 `@theme`). The JS preset above is the source
92
117
  of truth for WDK/QVAC.
@@ -0,0 +1,14 @@
1
+ //#region src/analytics/GtmLoader.d.ts
2
+ type GtmLoaderProps = {
3
+ gtmId: string;
4
+ };
5
+ /**
6
+ * Wraps `@next/third-parties` `GoogleTagManager` and delays mounting it until
7
+ * the page has fully loaded (`window.load`). The Next.js helper itself uses
8
+ * `next/script` with `afterInteractive`, which would otherwise inject GTM
9
+ * during hydration and contribute to TBT.
10
+ */
11
+ declare function GtmLoader({ gtmId }: GtmLoaderProps): import("react").JSX.Element | null;
12
+ //#endregion
13
+ export { GtmLoader, GtmLoaderProps };
14
+ //# sourceMappingURL=GtmLoader.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GtmLoader.d.mts","names":[],"sources":["../../src/analytics/GtmLoader.tsx"],"mappings":";KAKY;EACV;;;;;;;;iBASc,YAAY,SAAS,iCAAc,IAAA"}
@@ -0,0 +1,30 @@
1
+ "use client";
2
+ import { GoogleTagManager } from "@next/third-parties/google";
3
+ import { useEffect, useState } from "react";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/analytics/GtmLoader.tsx
6
+ /**
7
+ * Wraps `@next/third-parties` `GoogleTagManager` and delays mounting it until
8
+ * the page has fully loaded (`window.load`). The Next.js helper itself uses
9
+ * `next/script` with `afterInteractive`, which would otherwise inject GTM
10
+ * during hydration and contribute to TBT.
11
+ */
12
+ function GtmLoader({ gtmId }) {
13
+ const [ready, setReady] = useState(false);
14
+ useEffect(() => {
15
+ if (!gtmId) return;
16
+ const onLoad = () => setReady(true);
17
+ if (document.readyState === "complete") {
18
+ const id = window.setTimeout(onLoad, 0);
19
+ return () => window.clearTimeout(id);
20
+ }
21
+ window.addEventListener("load", onLoad, { once: true });
22
+ return () => window.removeEventListener("load", onLoad);
23
+ }, [gtmId]);
24
+ if (!ready) return null;
25
+ return /* @__PURE__ */ jsx(GoogleTagManager, { gtmId });
26
+ }
27
+ //#endregion
28
+ export { GtmLoader };
29
+
30
+ //# sourceMappingURL=GtmLoader.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GtmLoader.mjs","names":[],"sources":["../../src/analytics/GtmLoader.tsx"],"sourcesContent":["'use client'\n\nimport { GoogleTagManager } from '@next/third-parties/google'\nimport { useEffect, useState } from 'react'\n\nexport type GtmLoaderProps = {\n gtmId: string\n}\n\n/**\n * Wraps `@next/third-parties` `GoogleTagManager` and delays mounting it until\n * the page has fully loaded (`window.load`). The Next.js helper itself uses\n * `next/script` with `afterInteractive`, which would otherwise inject GTM\n * during hydration and contribute to TBT.\n */\nexport function GtmLoader({ gtmId }: GtmLoaderProps) {\n const [ready, setReady] = useState(false)\n\n useEffect(() => {\n if (!gtmId) return\n\n const onLoad = () => setReady(true)\n\n if (document.readyState === 'complete') {\n // Defer one tick so this doesn't fire a synchronous setState during the\n // effect (and to give the browser a frame to settle after `load`).\n const id = window.setTimeout(onLoad, 0)\n return () => window.clearTimeout(id)\n }\n\n window.addEventListener('load', onLoad, { once: true })\n return () => window.removeEventListener('load', onLoad)\n }, [gtmId])\n\n if (!ready) return null\n return <GoogleTagManager gtmId={gtmId} />\n}\n"],"mappings":";;;;;;;;;;;AAeA,SAAgB,UAAU,EAAE,SAAyB;CACnD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CAExC,gBAAgB;EACd,IAAI,CAAC,OAAO;EAEZ,MAAM,eAAe,SAAS,IAAI;EAElC,IAAI,SAAS,eAAe,YAAY;GAGtC,MAAM,KAAK,OAAO,WAAW,QAAQ,CAAC;GACtC,aAAa,OAAO,aAAa,EAAE;EACrC;EAEA,OAAO,iBAAiB,QAAQ,QAAQ,EAAE,MAAM,KAAK,CAAC;EACtD,aAAa,OAAO,oBAAoB,QAAQ,MAAM;CACxD,GAAG,CAAC,KAAK,CAAC;CAEV,IAAI,CAAC,OAAO,OAAO;CACnB,OAAO,oBAAC,kBAAD,EAAyB,MAAQ,CAAA;AAC1C"}
@@ -0,0 +1,46 @@
1
+ import { ComponentPropsWithoutRef, ElementType, ReactElement } from "react";
2
+ //#region src/analytics/createTrackedLink.d.ts
3
+ /**
4
+ * Discriminated tracking props. Apps keep tight `Section` / destination unions;
5
+ * pass them as generics to `createTrackedLink`.
6
+ */
7
+ type CreateTrackedEvent<TSection extends string, TDestination extends string> = {
8
+ event?: never;
9
+ section?: never;
10
+ destination?: never;
11
+ } | {
12
+ event: 'docs_clicked';
13
+ section: TSection;
14
+ destination?: string;
15
+ } | {
16
+ event: 'get_started_clicked';
17
+ section: TSection;
18
+ destination?: never;
19
+ } | {
20
+ event: 'external_link_clicked';
21
+ section: TSection;
22
+ destination: TDestination;
23
+ };
24
+ type TrackedCapture<TSection extends string, TDestination extends string> = {
25
+ docsClicked: (p: {
26
+ section: TSection;
27
+ destination?: string;
28
+ }) => void;
29
+ getStartedClicked: (p: {
30
+ section: TSection;
31
+ }) => void;
32
+ externalLinkClicked: (p: {
33
+ destination: TDestination;
34
+ section: TSection;
35
+ }) => void;
36
+ };
37
+ /**
38
+ * Builds a site `TrackedLink` that wraps the app's Link (`next-intl` navigation
39
+ * Link or `next/link`) and fires the provided capture callbacks.
40
+ *
41
+ * Routing and event catalogs stay in the app — core only owns the click wiring.
42
+ */
43
+ declare function createTrackedLink<TLink extends ElementType, TSection extends string = string, TDestination extends string = string>(Link: TLink, capture: TrackedCapture<TSection, TDestination>): (props: ComponentPropsWithoutRef<TLink> & CreateTrackedEvent<TSection, TDestination>) => ReactElement;
44
+ //#endregion
45
+ export { CreateTrackedEvent, type CreateTrackedEvent as TrackedEvent, TrackedCapture, createTrackedLink };
46
+ //# sourceMappingURL=createTrackedLink.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTrackedLink.d.mts","names":[],"sources":["../../src/analytics/createTrackedLink.tsx"],"mappings":";;;;;;KAcY,mBAAmB,yBAAyB;EAClD;EAAe;EAAiB;;EAChC;EAAuB,SAAS;EAAU;;EAC1C;EAA8B,SAAS;EAAU;;EAEjD;EACA,SAAS;EACT,aAAa;;KAGP,eAAe,yBAAyB;EAClD,cAAc;IAAK,SAAS;IAAU;;EACtC,oBAAoB;IAAK,SAAS;;EAClC,sBAAsB;IAAK,aAAa;IAAc,SAAS;;;;;;;;;iBASjD,kBACd,cAAc,aACd,kCACA,sCACA,MAAM,OAAO,SAAS,eAAe,UAAU,iBAsCxB,OAAK,yBAAA,SAAA,mBAAA,UAAA,kBAAuB"}
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { createElement } from "react";
3
+ //#region src/analytics/createTrackedLink.tsx
4
+ /**
5
+ * Builds a site `TrackedLink` that wraps the app's Link (`next-intl` navigation
6
+ * Link or `next/link`) and fires the provided capture callbacks.
7
+ *
8
+ * Routing and event catalogs stay in the app — core only owns the click wiring.
9
+ */
10
+ function createTrackedLink(Link, capture) {
11
+ function TrackedLink(props) {
12
+ const { event, section, destination, onClick, href, ...rest } = props;
13
+ const handleClick = (e) => {
14
+ if (event === "docs_clicked" && section) capture.docsClicked({
15
+ section,
16
+ destination: destination ?? String(href)
17
+ });
18
+ else if (event === "get_started_clicked" && section) capture.getStartedClicked({ section });
19
+ else if (event === "external_link_clicked" && section && destination) capture.externalLinkClicked({
20
+ section,
21
+ destination
22
+ });
23
+ onClick?.(e);
24
+ };
25
+ return createElement(Link, {
26
+ ...rest,
27
+ href,
28
+ onClick: handleClick
29
+ });
30
+ }
31
+ return TrackedLink;
32
+ }
33
+ //#endregion
34
+ export { createTrackedLink };
35
+
36
+ //# sourceMappingURL=createTrackedLink.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTrackedLink.mjs","names":[],"sources":["../../src/analytics/createTrackedLink.tsx"],"sourcesContent":["'use client'\n\nimport {\n createElement,\n type ComponentPropsWithoutRef,\n type ElementType,\n type MouseEvent,\n type ReactElement,\n} from 'react'\n\n/**\n * Discriminated tracking props. Apps keep tight `Section` / destination unions;\n * pass them as generics to `createTrackedLink`.\n */\nexport type CreateTrackedEvent<TSection extends string, TDestination extends string> =\n | { event?: never; section?: never; destination?: never }\n | { event: 'docs_clicked'; section: TSection; destination?: string }\n | { event: 'get_started_clicked'; section: TSection; destination?: never }\n | {\n event: 'external_link_clicked'\n section: TSection\n destination: TDestination\n }\n\nexport type TrackedCapture<TSection extends string, TDestination extends string> = {\n docsClicked: (p: { section: TSection; destination?: string }) => void\n getStartedClicked: (p: { section: TSection }) => void\n externalLinkClicked: (p: { destination: TDestination; section: TSection }) => void\n}\n\n/**\n * Builds a site `TrackedLink` that wraps the app's Link (`next-intl` navigation\n * Link or `next/link`) and fires the provided capture callbacks.\n *\n * Routing and event catalogs stay in the app — core only owns the click wiring.\n */\nexport function createTrackedLink<\n TLink extends ElementType,\n TSection extends string = string,\n TDestination extends string = string,\n>(Link: TLink, capture: TrackedCapture<TSection, TDestination>) {\n type LinkProps = ComponentPropsWithoutRef<TLink>\n type TrackedEvent = CreateTrackedEvent<TSection, TDestination>\n type TrackedLinkProps = LinkProps & TrackedEvent\n\n function TrackedLink(props: TrackedLinkProps): ReactElement {\n const { event, section, destination, onClick, href, ...rest } = props as TrackedLinkProps & {\n event?: TrackedEvent['event']\n section?: TSection\n destination?: string\n onClick?: (e: MouseEvent<HTMLAnchorElement>) => void\n href?: unknown\n }\n\n const handleClick = (e: MouseEvent<HTMLAnchorElement>) => {\n if (event === 'docs_clicked' && section) {\n capture.docsClicked({\n section,\n destination: (destination as string | undefined) ?? String(href),\n })\n } else if (event === 'get_started_clicked' && section) {\n capture.getStartedClicked({ section })\n } else if (event === 'external_link_clicked' && section && destination) {\n capture.externalLinkClicked({\n section,\n destination: destination as TDestination,\n })\n }\n onClick?.(e)\n }\n\n return createElement(Link, {\n ...(rest as LinkProps),\n href,\n onClick: handleClick,\n } as LinkProps)\n }\n\n return TrackedLink as (props: TrackedLinkProps) => ReactElement\n}\n\nexport type { CreateTrackedEvent as TrackedEvent }\n"],"mappings":";;;;;;;;;AAoCA,SAAgB,kBAId,MAAa,SAAiD;CAK9D,SAAS,YAAY,OAAuC;EAC1D,MAAM,EAAE,OAAO,SAAS,aAAa,SAAS,MAAM,GAAG,SAAS;EAQhE,MAAM,eAAe,MAAqC;GACxD,IAAI,UAAU,kBAAkB,SAC9B,QAAQ,YAAY;IAClB;IACA,aAAc,eAAsC,OAAO,IAAI;GACjE,CAAC;QACI,IAAI,UAAU,yBAAyB,SAC5C,QAAQ,kBAAkB,EAAE,QAAQ,CAAC;QAChC,IAAI,UAAU,2BAA2B,WAAW,aACzD,QAAQ,oBAAoB;IAC1B;IACa;GACf,CAAC;GAEH,UAAU,CAAC;EACb;EAEA,OAAO,cAAc,MAAM;GACzB,GAAI;GACJ;GACA,SAAS;EACX,CAAc;CAChB;CAEA,OAAO;AACT"}
@@ -0,0 +1,3 @@
1
+ import { GtmLoader, GtmLoaderProps } from "./GtmLoader.mjs";
2
+ import { CreateTrackedEvent, TrackedCapture, createTrackedLink } from "./createTrackedLink.mjs";
3
+ export { type CreateTrackedEvent, GtmLoader, type GtmLoaderProps, type TrackedCapture, type CreateTrackedEvent as TrackedEvent, createTrackedLink };
@@ -0,0 +1,3 @@
1
+ import { GtmLoader } from "./GtmLoader.mjs";
2
+ import { createTrackedLink } from "./createTrackedLink.mjs";
3
+ export { GtmLoader, createTrackedLink };
@@ -2,8 +2,8 @@ import { ComponentProps } from "react";
2
2
  import { VariantProps } from "class-variance-authority";
3
3
  //#region src/ui/components/Button/Button.d.ts
4
4
  declare const buttonVariants: (props?: ({
5
- variant?: "solid" | "outline" | "ghost" | "secondary" | "social" | "action" | "elevated-outline" | "link" | null | undefined;
6
- size?: "social" | "default" | "sm" | "cta" | "icon" | null | undefined;
5
+ variant?: "link" | "solid" | "outline" | "ghost" | "secondary" | "social" | "action" | "elevated-outline" | null | undefined;
6
+ size?: "sm" | "social" | "default" | "cta" | "icon" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  type ButtonProps = ComponentProps<'button'> & VariantProps<typeof buttonVariants> & {
9
9
  asChild?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"Button.d.mts","names":[],"sources":["../../../../src/ui/components/Button/Button.tsx"],"mappings":";;;cAUM,iBAAc;;;6CAmCnB;KAEW,cAAc,2BACxB,oBAAoB;EAClB;;iBAGK,SACP,WACA,SACA,MACA,YACG,SACF,8BAAW,IAAA"}
1
+ {"version":3,"file":"Button.d.mts","names":[],"sources":["../../../../src/ui/components/Button/Button.tsx"],"mappings":";;;cAUM,iBAAc;;;6CAuCnB;KAEW,cAAc,2BACxB,oBAAoB;EAClB;;iBAGK,SACP,WACA,SACA,MACA,YACG,SACF,8BAAW,IAAA"}
@@ -8,8 +8,8 @@ import { cva } from "class-variance-authority";
8
8
  const buttonVariants = cva(cn("inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors outline-none select-none disabled:pointer-events-none disabled:opacity-50", focusRingClassName), {
9
9
  variants: {
10
10
  variant: {
11
- "solid": "rounded-btn bg-accent text-accent-foreground hover:opacity-90",
12
- "outline": "rounded-btn border border-accent bg-transparent text-foreground hover:bg-foreground/5",
11
+ "solid": "rounded-btn bg-btn text-accent-foreground hover:opacity-90",
12
+ "outline": "rounded-btn border border-btn-secondary-border bg-transparent text-foreground hover:border-accent hover:bg-foreground/5",
13
13
  "ghost": "rounded-btn bg-transparent text-accent hover:bg-foreground/5",
14
14
  "secondary": "rounded-btn border border-foreground/20 bg-foreground/5 text-foreground hover:bg-foreground/10",
15
15
  "social": "rounded-btn border border-foreground/10 bg-card-elevated text-secondary hover:border-foreground/20 hover:text-foreground",
@@ -1 +1 @@
1
- {"version":3,"file":"Button.mjs","names":[],"sources":["../../../../src/ui/components/Button/Button.tsx"],"sourcesContent":["'use client'\n\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { focusRingClassName } from '../../a11y/focus.js'\nimport { cn } from '../../utils/cn.js'\n\nimport type { ComponentProps } from 'react'\n\nconst buttonVariants = cva(\n cn(\n 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors outline-none select-none disabled:pointer-events-none disabled:opacity-50',\n focusRingClassName\n ),\n {\n variants: {\n variant: {\n 'solid': 'rounded-btn bg-accent text-accent-foreground hover:opacity-90',\n 'outline':\n 'rounded-btn border border-accent bg-transparent text-foreground hover:bg-foreground/5',\n 'ghost': 'rounded-btn bg-transparent text-accent hover:bg-foreground/5',\n 'secondary':\n 'rounded-btn border border-foreground/20 bg-foreground/5 text-foreground hover:bg-foreground/10',\n 'social':\n 'rounded-btn border border-foreground/10 bg-card-elevated text-secondary hover:border-foreground/20 hover:text-foreground',\n 'action':\n 'rounded-btn border-0 bg-foreground text-background hover:opacity-90 no-underline',\n 'elevated-outline':\n 'rounded-btn border border-foreground bg-surface-elevated text-foreground hover:bg-surface-elevated/80',\n 'link': 'bg-transparent p-0 text-accent underline-offset-4 hover:underline',\n },\n size: {\n default: 'h-btn px-btn text-sm',\n sm: 'h-btn-sm px-btn-sm text-xs',\n cta: 'h-auto rounded-control px-4 py-3 text-sm font-medium',\n icon: 'size-9 rounded-btn p-0',\n social: 'h-[42px] gap-2 px-btn',\n },\n },\n defaultVariants: {\n variant: 'solid',\n size: 'default',\n },\n }\n)\n\nexport type ButtonProps = ComponentProps<'button'> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }\n\nfunction Button({\n className,\n variant = 'solid',\n size = 'default',\n asChild = false,\n ...props\n}: ButtonProps) {\n const Comp = asChild ? Slot : 'button'\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"],"mappings":";;;;;;;AAUA,MAAM,iBAAiB,IACrB,GACE,4LACA,kBACF,GACA;CACE,UAAU;EACR,SAAS;GACP,SAAS;GACT,WACE;GACF,SAAS;GACT,aACE;GACF,UACE;GACF,UACE;GACF,oBACE;GACF,QAAQ;EACV;EACA,MAAM;GACJ,SAAS;GACT,IAAI;GACJ,KAAK;GACL,MAAM;GACN,QAAQ;EACV;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;AAOA,SAAS,OAAO,EACd,WACA,UAAU,SACV,OAAO,WACP,UAAU,OACV,GAAG,SACW;CAGd,OACE,oBAHW,UAAU,OAAO,UAG5B;EACE,aAAU;EACV,gBAAc;EACd,aAAW;EACX,WAAW,GAAG,eAAe;GAAE;GAAS;EAAK,CAAC,GAAG,SAAS;EAC1D,GAAI;CACL,CAAA;AAEL"}
1
+ {"version":3,"file":"Button.mjs","names":[],"sources":["../../../../src/ui/components/Button/Button.tsx"],"sourcesContent":["'use client'\n\nimport { Slot } from '@radix-ui/react-slot'\nimport { cva, type VariantProps } from 'class-variance-authority'\n\nimport { focusRingClassName } from '../../a11y/focus.js'\nimport { cn } from '../../utils/cn.js'\n\nimport type { ComponentProps } from 'react'\n\nconst buttonVariants = cva(\n cn(\n 'inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap font-semibold transition-colors outline-none select-none disabled:pointer-events-none disabled:opacity-50',\n focusRingClassName\n ),\n {\n variants: {\n variant: {\n // Fill uses `--btn-primary-*` (accent fallback). Label keeps\n // `text-accent-foreground` so QVAC’s tailwind override\n // (`--color-text-on-action`) is unchanged; Pear already maps\n // accent.foreground → ink.\n 'solid': 'rounded-btn bg-btn text-accent-foreground hover:opacity-90',\n 'outline':\n 'rounded-btn border border-btn-secondary-border bg-transparent text-foreground hover:border-accent hover:bg-foreground/5',\n 'ghost': 'rounded-btn bg-transparent text-accent hover:bg-foreground/5',\n 'secondary':\n 'rounded-btn border border-foreground/20 bg-foreground/5 text-foreground hover:bg-foreground/10',\n 'social':\n 'rounded-btn border border-foreground/10 bg-card-elevated text-secondary hover:border-foreground/20 hover:text-foreground',\n 'action':\n 'rounded-btn border-0 bg-foreground text-background hover:opacity-90 no-underline',\n 'elevated-outline':\n 'rounded-btn border border-foreground bg-surface-elevated text-foreground hover:bg-surface-elevated/80',\n 'link': 'bg-transparent p-0 text-accent underline-offset-4 hover:underline',\n },\n size: {\n default: 'h-btn px-btn text-sm',\n sm: 'h-btn-sm px-btn-sm text-xs',\n cta: 'h-auto rounded-control px-4 py-3 text-sm font-medium',\n icon: 'size-9 rounded-btn p-0',\n social: 'h-[42px] gap-2 px-btn',\n },\n },\n defaultVariants: {\n variant: 'solid',\n size: 'default',\n },\n }\n)\n\nexport type ButtonProps = ComponentProps<'button'> &\n VariantProps<typeof buttonVariants> & {\n asChild?: boolean\n }\n\nfunction Button({\n className,\n variant = 'solid',\n size = 'default',\n asChild = false,\n ...props\n}: ButtonProps) {\n const Comp = asChild ? Slot : 'button'\n\n return (\n <Comp\n data-slot=\"button\"\n data-variant={variant}\n data-size={size}\n className={cn(buttonVariants({ variant, size }), className)}\n {...props}\n />\n )\n}\n\nexport { Button, buttonVariants }\n"],"mappings":";;;;;;;AAUA,MAAM,iBAAiB,IACrB,GACE,4LACA,kBACF,GACA;CACE,UAAU;EACR,SAAS;GAKP,SAAS;GACT,WACE;GACF,SAAS;GACT,aACE;GACF,UACE;GACF,UACE;GACF,oBACE;GACF,QAAQ;EACV;EACA,MAAM;GACJ,SAAS;GACT,IAAI;GACJ,KAAK;GACL,MAAM;GACN,QAAQ;EACV;CACF;CACA,iBAAiB;EACf,SAAS;EACT,MAAM;CACR;AACF,CACF;AAOA,SAAS,OAAO,EACd,WACA,UAAU,SACV,OAAO,WACP,UAAU,OACV,GAAG,SACW;CAGd,OACE,oBAHW,UAAU,OAAO,UAG5B;EACE,aAAU;EACV,gBAAc;EACd,aAAW;EACX,WAAW,GAAG,eAAe;GAAE;GAAS;EAAK,CAAC,GAAG,SAAS;EAC1D,GAAI;CACL,CAAA;AAEL"}
@@ -3,7 +3,7 @@ import { VariantProps } from "class-variance-authority";
3
3
  //#region src/ui/components/Card/Card.d.ts
4
4
  declare const cardVariants: (props?: ({
5
5
  variant?: "surface" | "card" | "elevated" | null | undefined;
6
- padding?: "sm" | "none" | "md" | "lg" | null | undefined;
6
+ padding?: "none" | "sm" | "md" | "lg" | null | undefined;
7
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
8
8
  type CardProps = HTMLAttributes<HTMLDivElement> & VariantProps<typeof cardVariants> & {
9
9
  asChild?: boolean;
@@ -22,7 +22,7 @@ function Input({ className, error, leftIcon, rightIcon, id, ref, ...props }) {
22
22
  "data-slot": "input",
23
23
  "aria-invalid": hasError || void 0,
24
24
  "aria-describedby": hasError && errorId ? errorId : void 0,
25
- className: cn("h-11 w-full rounded-btn border border-border-subtle bg-background px-3.5 text-sm text-foreground", "placeholder:text-muted", "transition-colors hover:border-border", "focus-visible:border-accent focus-visible:outline-none", "disabled:cursor-not-allowed disabled:opacity-50", hasError && "border-destructive focus-visible:border-destructive", leftIcon && "pl-11", rightIcon && "pr-11", className),
25
+ className: cn("h-11 w-full rounded-btn border border-control-border bg-background px-3.5 text-sm text-foreground", "placeholder:text-muted", "transition-colors hover:border-border", "focus-visible:border-accent focus-visible:outline-none", "disabled:cursor-not-allowed disabled:opacity-50", hasError && "border-destructive focus-visible:border-destructive", leftIcon && "pl-11", rightIcon && "pr-11", className),
26
26
  ...props
27
27
  }),
28
28
  rightIcon ? /* @__PURE__ */ jsx("span", {
@@ -1 +1 @@
1
- {"version":3,"file":"Input.mjs","names":[],"sources":["../../../../src/ui/components/Input/Input.tsx"],"sourcesContent":["'use client'\n\nimport { Primitive } from '@radix-ui/react-primitive'\n\nimport { cn } from '../../utils/cn.js'\n\nimport type { ComponentProps, ReactNode } from 'react'\n\nexport type InputProps = Omit<ComponentProps<typeof Primitive.input>, 'children'> & {\n error?: boolean | string\n leftIcon?: ReactNode\n rightIcon?: ReactNode\n}\n\nfunction Input({ className, error, leftIcon, rightIcon, id, ref, ...props }: InputProps) {\n const errorId = id ? `${id}-error` : undefined\n const hasError = Boolean(error)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <div className=\"relative flex items-center\">\n {leftIcon ? (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute left-4 flex items-center\"\n >\n {leftIcon}\n </span>\n ) : null}\n\n <Primitive.input\n ref={ref}\n id={id}\n data-slot=\"input\"\n aria-invalid={hasError || undefined}\n aria-describedby={hasError && errorId ? errorId : undefined}\n className={cn(\n 'h-11 w-full rounded-btn border border-border-subtle bg-background px-3.5 text-sm text-foreground',\n 'placeholder:text-muted',\n 'transition-colors hover:border-border',\n 'focus-visible:border-accent focus-visible:outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n hasError && 'border-destructive focus-visible:border-destructive',\n leftIcon && 'pl-11',\n rightIcon && 'pr-11',\n className\n )}\n {...props}\n />\n\n {rightIcon ? (\n <span className=\"absolute right-3.5 flex items-center\">{rightIcon}</span>\n ) : null}\n </div>\n\n {typeof error === 'string' ? (\n <p id={errorId} className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n )\n}\n\nexport { Input }\n"],"mappings":";;;;;AAcA,SAAS,MAAM,EAAE,WAAW,OAAO,UAAU,WAAW,IAAI,KAAK,GAAG,SAAqB;CACvF,MAAM,UAAU,KAAK,GAAG,GAAG,UAAU;CACrC,MAAM,WAAW,QAAQ,KAAK;CAE9B,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACG,WACC,oBAAC,QAAD;KACE,eAAY;KACZ,WAAU;KAET,UAAA;IACG,CAAA,IACJ;IAEJ,oBAAC,UAAU,OAAX;KACO;KACD;KACJ,aAAU;KACV,gBAAc,YAAY;KAC1B,oBAAkB,YAAY,UAAU,UAAU;KAClD,WAAW,GACT,oGACA,0BACA,yCACA,0DACA,mDACA,YAAY,uDACZ,YAAY,SACZ,aAAa,SACb,SACF;KACA,GAAI;IACL,CAAA;IAEA,YACC,oBAAC,QAAD;KAAM,WAAU;KAAwC,UAAA;IAAgB,CAAA,IACtE;GACD;EAEJ,CAAA,GAAA,OAAO,UAAU,WAChB,oBAAC,KAAD;GAAG,IAAI;GAAS,WAAU;GACvB,UAAA;EACA,CAAA,IACD,IACD;;AAET"}
1
+ {"version":3,"file":"Input.mjs","names":[],"sources":["../../../../src/ui/components/Input/Input.tsx"],"sourcesContent":["'use client'\n\nimport { Primitive } from '@radix-ui/react-primitive'\n\nimport { cn } from '../../utils/cn.js'\n\nimport type { ComponentProps, ReactNode } from 'react'\n\nexport type InputProps = Omit<ComponentProps<typeof Primitive.input>, 'children'> & {\n error?: boolean | string\n leftIcon?: ReactNode\n rightIcon?: ReactNode\n}\n\nfunction Input({ className, error, leftIcon, rightIcon, id, ref, ...props }: InputProps) {\n const errorId = id ? `${id}-error` : undefined\n const hasError = Boolean(error)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <div className=\"relative flex items-center\">\n {leftIcon ? (\n <span\n aria-hidden=\"true\"\n className=\"pointer-events-none absolute left-4 flex items-center\"\n >\n {leftIcon}\n </span>\n ) : null}\n\n <Primitive.input\n ref={ref}\n id={id}\n data-slot=\"input\"\n aria-invalid={hasError || undefined}\n aria-describedby={hasError && errorId ? errorId : undefined}\n className={cn(\n 'h-11 w-full rounded-btn border border-control-border bg-background px-3.5 text-sm text-foreground',\n 'placeholder:text-muted',\n 'transition-colors hover:border-border',\n 'focus-visible:border-accent focus-visible:outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n hasError && 'border-destructive focus-visible:border-destructive',\n leftIcon && 'pl-11',\n rightIcon && 'pr-11',\n className\n )}\n {...props}\n />\n\n {rightIcon ? (\n <span className=\"absolute right-3.5 flex items-center\">{rightIcon}</span>\n ) : null}\n </div>\n\n {typeof error === 'string' ? (\n <p id={errorId} className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n )\n}\n\nexport { Input }\n"],"mappings":";;;;;AAcA,SAAS,MAAM,EAAE,WAAW,OAAO,UAAU,WAAW,IAAI,KAAK,GAAG,SAAqB;CACvF,MAAM,UAAU,KAAK,GAAG,GAAG,UAAU;CACrC,MAAM,WAAW,QAAQ,KAAK;CAE9B,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA;IACG,WACC,oBAAC,QAAD;KACE,eAAY;KACZ,WAAU;KAET,UAAA;IACG,CAAA,IACJ;IAEJ,oBAAC,UAAU,OAAX;KACO;KACD;KACJ,aAAU;KACV,gBAAc,YAAY;KAC1B,oBAAkB,YAAY,UAAU,UAAU;KAClD,WAAW,GACT,qGACA,0BACA,yCACA,0DACA,mDACA,YAAY,uDACZ,YAAY,SACZ,aAAa,SACb,SACF;KACA,GAAI;IACL,CAAA;IAEA,YACC,oBAAC,QAAD;KAAM,WAAU;KAAwC,UAAA;IAAgB,CAAA,IACtE;GACD;EAEJ,CAAA,GAAA,OAAO,UAAU,WAChB,oBAAC,KAAD;GAAG,IAAI;GAAS,WAAU;GACvB,UAAA;EACA,CAAA,IACD,IACD;;AAET"}
@@ -12,7 +12,7 @@ function Textarea({ className, error, id, ref, ...props }) {
12
12
  "data-slot": "textarea",
13
13
  "aria-invalid": hasError || void 0,
14
14
  "aria-describedby": hasError && errorId ? errorId : void 0,
15
- className: cn("min-h-24 w-full resize-y rounded-btn border border-border-subtle bg-background px-3.5 py-3 text-sm text-foreground", "placeholder:text-muted", "transition-colors hover:border-border", "focus-visible:border-accent focus-visible:outline-none", "disabled:cursor-not-allowed disabled:opacity-50", hasError && "border-destructive focus-visible:border-destructive", className),
15
+ className: cn("min-h-24 w-full resize-y rounded-btn border border-control-border bg-background px-3.5 py-3 text-sm text-foreground", "placeholder:text-muted", "transition-colors hover:border-border", "focus-visible:border-accent focus-visible:outline-none", "disabled:cursor-not-allowed disabled:opacity-50", hasError && "border-destructive focus-visible:border-destructive", className),
16
16
  ...props
17
17
  }), typeof error === "string" ? /* @__PURE__ */ jsx("p", {
18
18
  id: errorId,
@@ -1 +1 @@
1
- {"version":3,"file":"Textarea.mjs","names":[],"sources":["../../../../src/ui/components/Textarea/Textarea.tsx"],"sourcesContent":["import { cn } from '../../utils/cn.js'\n\nimport type { ComponentProps } from 'react'\n\nexport type TextareaProps = Omit<ComponentProps<'textarea'>, 'children'> & {\n error?: boolean | string\n}\n\nfunction Textarea({ className, error, id, ref, ...props }: TextareaProps) {\n const errorId = id ? `${id}-error` : undefined\n const hasError = Boolean(error)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <textarea\n ref={ref}\n id={id}\n data-slot=\"textarea\"\n aria-invalid={hasError || undefined}\n aria-describedby={hasError && errorId ? errorId : undefined}\n className={cn(\n 'min-h-24 w-full resize-y rounded-btn border border-border-subtle bg-background px-3.5 py-3 text-sm text-foreground',\n 'placeholder:text-muted',\n 'transition-colors hover:border-border',\n 'focus-visible:border-accent focus-visible:outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n hasError && 'border-destructive focus-visible:border-destructive',\n className\n )}\n {...props}\n />\n\n {typeof error === 'string' ? (\n <p id={errorId} className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n )\n}\n\nexport { Textarea }\n"],"mappings":";;;AAQA,SAAS,SAAS,EAAE,WAAW,OAAO,IAAI,KAAK,GAAG,SAAwB;CACxE,MAAM,UAAU,KAAK,GAAG,GAAG,UAAU;CACrC,MAAM,WAAW,QAAQ,KAAK;CAE9B,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,YAAD;GACO;GACD;GACJ,aAAU;GACV,gBAAc,YAAY;GAC1B,oBAAkB,YAAY,UAAU,UAAU;GAClD,WAAW,GACT,sHACA,0BACA,yCACA,0DACA,mDACA,YAAY,uDACZ,SACF;GACA,GAAI;EACL,CAAA,GAEA,OAAO,UAAU,WAChB,oBAAC,KAAD;GAAG,IAAI;GAAS,WAAU;GACvB,UAAA;EACA,CAAA,IACD,IACD;;AAET"}
1
+ {"version":3,"file":"Textarea.mjs","names":[],"sources":["../../../../src/ui/components/Textarea/Textarea.tsx"],"sourcesContent":["import { cn } from '../../utils/cn.js'\n\nimport type { ComponentProps } from 'react'\n\nexport type TextareaProps = Omit<ComponentProps<'textarea'>, 'children'> & {\n error?: boolean | string\n}\n\nfunction Textarea({ className, error, id, ref, ...props }: TextareaProps) {\n const errorId = id ? `${id}-error` : undefined\n const hasError = Boolean(error)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <textarea\n ref={ref}\n id={id}\n data-slot=\"textarea\"\n aria-invalid={hasError || undefined}\n aria-describedby={hasError && errorId ? errorId : undefined}\n className={cn(\n 'min-h-24 w-full resize-y rounded-btn border border-control-border bg-background px-3.5 py-3 text-sm text-foreground',\n 'placeholder:text-muted',\n 'transition-colors hover:border-border',\n 'focus-visible:border-accent focus-visible:outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n hasError && 'border-destructive focus-visible:border-destructive',\n className\n )}\n {...props}\n />\n\n {typeof error === 'string' ? (\n <p id={errorId} className=\"text-xs text-destructive\">\n {error}\n </p>\n ) : null}\n </div>\n )\n}\n\nexport { Textarea }\n"],"mappings":";;;AAQA,SAAS,SAAS,EAAE,WAAW,OAAO,IAAI,KAAK,GAAG,SAAwB;CACxE,MAAM,UAAU,KAAK,GAAG,GAAG,UAAU;CACrC,MAAM,WAAW,QAAQ,KAAK;CAE9B,OACE,qBAAC,OAAD;EAAK,WAAU;EAAf,UAAA,CACE,oBAAC,YAAD;GACO;GACD;GACJ,aAAU;GACV,gBAAc,YAAY;GAC1B,oBAAkB,YAAY,UAAU,UAAU;GAClD,WAAW,GACT,uHACA,0BACA,yCACA,0DACA,mDACA,YAAY,uDACZ,SACF;GACA,GAAI;EACL,CAAA,GAEA,OAAO,UAAU,WAChB,oBAAC,KAAD;GAAG,IAAI;GAAS,WAAU;GACvB,UAAA;EACA,CAAA,IACD,IACD;;AAET"}
@@ -14,6 +14,10 @@
14
14
  *
15
15
  * CSS: define the `--color-*` / `--primitive-*` / `--btn-*` vars the preset
16
16
  * references (see WDK `globals.css`), or remap them in the app `:root`.
17
+ *
18
+ * Button fills/borders prefer `--btn-primary-*` / `--btn-secondary-*` and fall
19
+ * back to `--color-accent` so sites that only set accent keep today’s look.
20
+ * Override those `--btn-*` vars (e.g. Pear outline = gray) without forking Button.
17
21
  */
18
22
  declare const preset: {
19
23
  theme: {
@@ -37,6 +41,27 @@ declare const preset: {
37
41
  hover: string;
38
42
  dim: string;
39
43
  };
44
+ /**
45
+ * Solid CTA fill. Defaults to accent when `--btn-primary-*` are unset
46
+ * (QVAC) or alias accent (WDK/MDK).
47
+ */
48
+ btn: {
49
+ DEFAULT: string;
50
+ hover: string;
51
+ foreground: string;
52
+ };
53
+ /** Outline CTA border/text. Defaults to accent border + primary text. */
54
+ 'btn-secondary': {
55
+ border: string;
56
+ DEFAULT: string;
57
+ };
58
+ /**
59
+ * Form control stroke. Defaults to border-subtle (current Input/Textarea).
60
+ * Set `--control-border` to remap without CSS attribute overrides.
61
+ */
62
+ control: {
63
+ border: string;
64
+ };
40
65
  border: {
41
66
  DEFAULT: string;
42
67
  subtle: string;
@@ -1 +1 @@
1
- {"version":3,"file":"preset.d.mts","names":[],"sources":["../../../src/ui/theme/preset.ts"],"mappings":";;;;;;;;;;;;;;;;;cAoBM"}
1
+ {"version":3,"file":"preset.d.mts","names":[],"sources":["../../../src/ui/theme/preset.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;cA2BM"}
@@ -14,9 +14,15 @@
14
14
  *
15
15
  * CSS: define the `--color-*` / `--primitive-*` / `--btn-*` vars the preset
16
16
  * references (see WDK `globals.css`), or remap them in the app `:root`.
17
+ *
18
+ * Button fills/borders prefer `--btn-primary-*` / `--btn-secondary-*` and fall
19
+ * back to `--color-accent` so sites that only set accent keep today’s look.
20
+ * Override those `--btn-*` vars (e.g. Pear outline = gray) without forking Button.
17
21
  */
18
22
  const hsl = (name) => `hsl(var(${name}) / <alpha-value>)`;
19
23
  const cssVar = (name) => `var(${name})`;
24
+ /** HSL channel var with fallback channel var (both unwrapped). */
25
+ const hslFallback = (name, fallback) => `hsl(var(${name}, var(${fallback})) / <alpha-value>)`;
20
26
  const preset = { theme: { extend: {
21
27
  colors: {
22
28
  "background": hsl("--color-bg-base"),
@@ -37,6 +43,25 @@ const preset = { theme: { extend: {
37
43
  hover: hsl("--color-accent-hover"),
38
44
  dim: hsl("--color-accent-dim")
39
45
  },
46
+ /**
47
+ * Solid CTA fill. Defaults to accent when `--btn-primary-*` are unset
48
+ * (QVAC) or alias accent (WDK/MDK).
49
+ */
50
+ "btn": {
51
+ DEFAULT: hslFallback("--btn-primary-bg", "--color-accent"),
52
+ hover: hslFallback("--btn-primary-bg-hover", "--color-accent-hover"),
53
+ foreground: hslFallback("--btn-primary-text", "--color-text-primary")
54
+ },
55
+ /** Outline CTA border/text. Defaults to accent border + primary text. */
56
+ "btn-secondary": {
57
+ border: hslFallback("--btn-secondary-border", "--color-accent"),
58
+ DEFAULT: hslFallback("--btn-secondary-text", "--color-text-primary")
59
+ },
60
+ /**
61
+ * Form control stroke. Defaults to border-subtle (current Input/Textarea).
62
+ * Set `--control-border` to remap without CSS attribute overrides.
63
+ */
64
+ "control": { border: hslFallback("--control-border", "--color-border-subtle") },
40
65
  "border": {
41
66
  DEFAULT: hsl("--color-border-default"),
42
67
  subtle: hsl("--color-border-subtle")
@@ -1 +1 @@
1
- {"version":3,"file":"preset.mjs","names":[],"sources":["../../../src/ui/theme/preset.ts"],"sourcesContent":["/**\n * Shared websites UI Tailwind preset — full theme contract, WDK defaults.\n *\n * Same utility names everywhere (`bg-accent`, `text-foreground`, `text-display-1`,\n * `rounded-btn`, `h-btn`, …). Apps point CSS variables at their brand palette.\n *\n * // WDK — defaults already match site tokens\n * presets: [uiPreset]\n *\n * // QVAC — same keys, override color (and any other) values\n * presets: [uiPreset]\n * theme: { extend: { colors: { accent: { DEFAULT: '…' }, … } } }\n *\n * CSS: define the `--color-*` / `--primitive-*` / `--btn-*` vars the preset\n * references (see WDK `globals.css`), or remap them in the app `:root`.\n */\n\nconst hsl = (name: string) => `hsl(var(${name}) / <alpha-value>)`\nconst cssVar = (name: string) => `var(${name})`\n\nconst preset = {\n theme: {\n extend: {\n colors: {\n 'background': hsl('--color-bg-base'),\n 'recessed': hsl('--color-bg-recessed'),\n 'surface': hsl('--color-bg-surface'),\n 'surface-elevated': hsl('--color-bg-card-elevated'),\n 'card': hsl('--color-bg-card'),\n 'card-elevated': hsl('--color-bg-card-elevated'),\n 'foreground': hsl('--color-text-primary'),\n 'secondary': hsl('--color-text-secondary'),\n 'muted': hsl('--color-text-muted'),\n 'landing-text': hsl('--color-landing-text'),\n 'landing-text-light': hsl('--color-landing-text-light'),\n 'landing-text-light-30': cssVar('--landing-text-light-30'),\n\n 'accent': {\n DEFAULT: hsl('--color-accent'),\n foreground: hsl('--color-text-primary'),\n hover: hsl('--color-accent-hover'),\n dim: hsl('--color-accent-dim'),\n },\n 'border': {\n DEFAULT: hsl('--color-border-default'),\n subtle: hsl('--color-border-subtle'),\n },\n 'success': hsl('--color-success'),\n 'warning': hsl('--color-warning'),\n 'error': hsl('--color-error'),\n 'destructive': hsl('--color-error'),\n 'info': hsl('--color-info'),\n 'ring': hsl('--color-accent'),\n\n 'teal': hsl('--color-teal'),\n 'amber': hsl('--color-amber'),\n 'purple': hsl('--color-purple'),\n 'green': hsl('--color-green'),\n\n 'blog-primary': hsl('--color-blog-primary'),\n 'blog-body': hsl('--color-blog-body'),\n 'blog-meta': hsl('--color-blog-meta'),\n\n 'primitive': {\n 'black': hsl('--primitive-black'),\n 'dark-grey': hsl('--primitive-dark-grey'),\n 'grey': hsl('--primitive-grey'),\n 'light-grey': hsl('--primitive-light-grey'),\n 'white': hsl('--primitive-white'),\n 'orange': hsl('--primitive-orange'),\n 'orange-dark': hsl('--primitive-orange-dark'),\n 'purple': hsl('--primitive-purple'),\n },\n },\n\n fontFamily: {\n sans: ['var(--font-sans)', 'sans-serif'],\n display: ['var(--font-sharp)', 'sans-serif'],\n mono: ['var(--font-mono)', 'monospace'],\n },\n\n fontSize: {\n 'display-1': ['60px', { lineHeight: '1.05', letterSpacing: '0' }],\n 'display-2': ['50px', { lineHeight: '1.05', letterSpacing: '0' }],\n 'heading-3': ['38px', { lineHeight: '40px', letterSpacing: '0' }],\n 'subhead-1': ['28px', { lineHeight: '32px', letterSpacing: '0' }],\n 'subhead-2': ['22px', { lineHeight: '26px', letterSpacing: '0' }],\n 'subhead-3': ['13px', { lineHeight: '1', letterSpacing: '2px' }],\n 'body-1': ['22px', { lineHeight: '32px' }],\n 'body-2': ['18px', { lineHeight: '28px' }],\n 'body-sm': ['15px', { lineHeight: '20px' }],\n 'body-md': ['14px', { lineHeight: '20px' }],\n 'body-xs': ['13px', { lineHeight: '18px' }],\n 'btn': ['14px', { lineHeight: '19px', letterSpacing: '0.5px' }],\n 'btn-cta': ['15px', { lineHeight: '1' }],\n 'code': ['17px', { lineHeight: '20px' }],\n 'label': ['11px', { lineHeight: '16px', letterSpacing: '0.3px' }],\n 'badge': ['10px', { lineHeight: '1', letterSpacing: '0.4px' }],\n 'caption': ['12px', { lineHeight: '16px' }],\n 'inter-h1': ['50px', { lineHeight: '60px' }],\n 'inter-h2': ['30px', { lineHeight: '38px' }],\n 'inter-h3': ['26px', { lineHeight: '36px' }],\n 'blog-mob': ['16px', { lineHeight: '24px' }],\n },\n\n borderRadius: {\n btn: '5px',\n card: '10px',\n badge: '5px',\n control: 'var(--control-radius)',\n pill: '9999px',\n },\n\n height: {\n 'btn': 'var(--btn-primary-height)',\n 'btn-sm': 'var(--btn-sm-height)',\n },\n\n size: {\n 'btn-sm': 'var(--btn-sm-height)',\n },\n\n padding: {\n 'btn': 'var(--btn-primary-px)',\n 'btn-sm': 'var(--btn-sm-px)',\n },\n },\n },\n}\n\nexport default preset\n"],"mappings":"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"}
1
+ {"version":3,"file":"preset.mjs","names":[],"sources":["../../../src/ui/theme/preset.ts"],"sourcesContent":["/**\n * Shared websites UI Tailwind preset — full theme contract, WDK defaults.\n *\n * Same utility names everywhere (`bg-accent`, `text-foreground`, `text-display-1`,\n * `rounded-btn`, `h-btn`, …). Apps point CSS variables at their brand palette.\n *\n * // WDK — defaults already match site tokens\n * presets: [uiPreset]\n *\n * // QVAC — same keys, override color (and any other) values\n * presets: [uiPreset]\n * theme: { extend: { colors: { accent: { DEFAULT: '…' }, … } } }\n *\n * CSS: define the `--color-*` / `--primitive-*` / `--btn-*` vars the preset\n * references (see WDK `globals.css`), or remap them in the app `:root`.\n *\n * Button fills/borders prefer `--btn-primary-*` / `--btn-secondary-*` and fall\n * back to `--color-accent` so sites that only set accent keep today’s look.\n * Override those `--btn-*` vars (e.g. Pear outline = gray) without forking Button.\n */\n\nconst hsl = (name: string) => `hsl(var(${name}) / <alpha-value>)`\nconst cssVar = (name: string) => `var(${name})`\n/** HSL channel var with fallback channel var (both unwrapped). */\nconst hslFallback = (name: string, fallback: string) =>\n `hsl(var(${name}, var(${fallback})) / <alpha-value>)`\n\nconst preset = {\n theme: {\n extend: {\n colors: {\n 'background': hsl('--color-bg-base'),\n 'recessed': hsl('--color-bg-recessed'),\n 'surface': hsl('--color-bg-surface'),\n 'surface-elevated': hsl('--color-bg-card-elevated'),\n 'card': hsl('--color-bg-card'),\n 'card-elevated': hsl('--color-bg-card-elevated'),\n 'foreground': hsl('--color-text-primary'),\n 'secondary': hsl('--color-text-secondary'),\n 'muted': hsl('--color-text-muted'),\n 'landing-text': hsl('--color-landing-text'),\n 'landing-text-light': hsl('--color-landing-text-light'),\n 'landing-text-light-30': cssVar('--landing-text-light-30'),\n\n 'accent': {\n DEFAULT: hsl('--color-accent'),\n foreground: hsl('--color-text-primary'),\n hover: hsl('--color-accent-hover'),\n dim: hsl('--color-accent-dim'),\n },\n\n /**\n * Solid CTA fill. Defaults to accent when `--btn-primary-*` are unset\n * (QVAC) or alias accent (WDK/MDK).\n */\n 'btn': {\n DEFAULT: hslFallback('--btn-primary-bg', '--color-accent'),\n hover: hslFallback('--btn-primary-bg-hover', '--color-accent-hover'),\n foreground: hslFallback('--btn-primary-text', '--color-text-primary'),\n },\n /** Outline CTA border/text. Defaults to accent border + primary text. */\n 'btn-secondary': {\n border: hslFallback('--btn-secondary-border', '--color-accent'),\n DEFAULT: hslFallback('--btn-secondary-text', '--color-text-primary'),\n },\n /**\n * Form control stroke. Defaults to border-subtle (current Input/Textarea).\n * Set `--control-border` to remap without CSS attribute overrides.\n */\n 'control': {\n border: hslFallback('--control-border', '--color-border-subtle'),\n },\n\n 'border': {\n DEFAULT: hsl('--color-border-default'),\n subtle: hsl('--color-border-subtle'),\n },\n 'success': hsl('--color-success'),\n 'warning': hsl('--color-warning'),\n 'error': hsl('--color-error'),\n 'destructive': hsl('--color-error'),\n 'info': hsl('--color-info'),\n 'ring': hsl('--color-accent'),\n\n 'teal': hsl('--color-teal'),\n 'amber': hsl('--color-amber'),\n 'purple': hsl('--color-purple'),\n 'green': hsl('--color-green'),\n\n 'blog-primary': hsl('--color-blog-primary'),\n 'blog-body': hsl('--color-blog-body'),\n 'blog-meta': hsl('--color-blog-meta'),\n\n 'primitive': {\n 'black': hsl('--primitive-black'),\n 'dark-grey': hsl('--primitive-dark-grey'),\n 'grey': hsl('--primitive-grey'),\n 'light-grey': hsl('--primitive-light-grey'),\n 'white': hsl('--primitive-white'),\n 'orange': hsl('--primitive-orange'),\n 'orange-dark': hsl('--primitive-orange-dark'),\n 'purple': hsl('--primitive-purple'),\n },\n },\n\n fontFamily: {\n sans: ['var(--font-sans)', 'sans-serif'],\n display: ['var(--font-sharp)', 'sans-serif'],\n mono: ['var(--font-mono)', 'monospace'],\n },\n\n fontSize: {\n 'display-1': ['60px', { lineHeight: '1.05', letterSpacing: '0' }],\n 'display-2': ['50px', { lineHeight: '1.05', letterSpacing: '0' }],\n 'heading-3': ['38px', { lineHeight: '40px', letterSpacing: '0' }],\n 'subhead-1': ['28px', { lineHeight: '32px', letterSpacing: '0' }],\n 'subhead-2': ['22px', { lineHeight: '26px', letterSpacing: '0' }],\n 'subhead-3': ['13px', { lineHeight: '1', letterSpacing: '2px' }],\n 'body-1': ['22px', { lineHeight: '32px' }],\n 'body-2': ['18px', { lineHeight: '28px' }],\n 'body-sm': ['15px', { lineHeight: '20px' }],\n 'body-md': ['14px', { lineHeight: '20px' }],\n 'body-xs': ['13px', { lineHeight: '18px' }],\n 'btn': ['14px', { lineHeight: '19px', letterSpacing: '0.5px' }],\n 'btn-cta': ['15px', { lineHeight: '1' }],\n 'code': ['17px', { lineHeight: '20px' }],\n 'label': ['11px', { lineHeight: '16px', letterSpacing: '0.3px' }],\n 'badge': ['10px', { lineHeight: '1', letterSpacing: '0.4px' }],\n 'caption': ['12px', { lineHeight: '16px' }],\n 'inter-h1': ['50px', { lineHeight: '60px' }],\n 'inter-h2': ['30px', { lineHeight: '38px' }],\n 'inter-h3': ['26px', { lineHeight: '36px' }],\n 'blog-mob': ['16px', { lineHeight: '24px' }],\n },\n\n borderRadius: {\n // Keep 5px default (current WDK/MDK render). Apps override via\n // theme.extend.borderRadius.btn (QVAC / Pear) — do not read\n // `--btn-primary-radius` here so WDK’s declared 4px token does not\n // silently change live buttons from 5px → 4px.\n btn: '5px',\n card: '10px',\n badge: '5px',\n control: 'var(--control-radius)',\n pill: '9999px',\n },\n\n height: {\n 'btn': 'var(--btn-primary-height)',\n 'btn-sm': 'var(--btn-sm-height)',\n },\n\n size: {\n 'btn-sm': 'var(--btn-sm-height)',\n },\n\n padding: {\n 'btn': 'var(--btn-primary-px)',\n 'btn-sm': 'var(--btn-sm-px)',\n },\n },\n },\n}\n\nexport default preset\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tetherto/dev-websites-core",
3
- "version": "0.3.0",
4
- "description": "Shared CMS, SEO, i18n, utils, and UI primitives for the WDK / QVAC websites.",
3
+ "version": "0.3.2",
4
+ "description": "Shared CMS, SEO, i18n, utils, UI, and analytics primitives for the WDK / QVAC / PEAR / MDK websites.",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -43,6 +43,11 @@
43
43
  "import": "./dist/utils/index.mjs",
44
44
  "default": "./dist/utils/index.mjs"
45
45
  },
46
+ "./analytics": {
47
+ "types": "./dist/analytics/index.d.mts",
48
+ "import": "./dist/analytics/index.mjs",
49
+ "default": "./dist/analytics/index.mjs"
50
+ },
46
51
  "./ui": {
47
52
  "types": "./dist/ui/index.d.mts",
48
53
  "import": "./dist/ui/index.mjs",
@@ -83,6 +88,7 @@
83
88
  },
84
89
  "peerDependencies": {
85
90
  "@aws-sdk/client-s3": "^3.0.0",
91
+ "@next/third-parties": ">=15",
86
92
  "@payloadcms/richtext-lexical": "^3.85.1",
87
93
  "@payloadcms/storage-s3": "^3.85.1",
88
94
  "@radix-ui/react-dialog": "^1.1.0",
@@ -101,6 +107,9 @@
101
107
  "tailwind-merge": "^3.0.0"
102
108
  },
103
109
  "peerDependenciesMeta": {
110
+ "@next/third-parties": {
111
+ "optional": true
112
+ },
104
113
  "embla-carousel-react": {
105
114
  "optional": true
106
115
  },
@@ -111,6 +120,7 @@
111
120
  "devDependencies": {
112
121
  "@aws-sdk/client-s3": "^3.787.0",
113
122
  "@eslint/js": "^9.39.0",
123
+ "@next/third-parties": "^16.3.2",
114
124
  "@payloadcms/richtext-lexical": "3.88.0",
115
125
  "@payloadcms/storage-s3": "3.88.0",
116
126
  "@radix-ui/react-dialog": "^1.1.15",
@@ -13,10 +13,17 @@
13
13
  *
14
14
  * CSS: define the `--color-*` / `--primitive-*` / `--btn-*` vars the preset
15
15
  * references (see WDK `globals.css`), or remap them in the app `:root`.
16
+ *
17
+ * Button fills/borders prefer `--btn-primary-*` / `--btn-secondary-*` and fall
18
+ * back to `--color-accent` so sites that only set accent keep today’s look.
19
+ * Override those `--btn-*` vars (e.g. Pear outline = gray) without forking Button.
16
20
  */
17
21
 
18
22
  const hsl = (name: string) => `hsl(var(${name}) / <alpha-value>)`
19
23
  const cssVar = (name: string) => `var(${name})`
24
+ /** HSL channel var with fallback channel var (both unwrapped). */
25
+ const hslFallback = (name: string, fallback: string) =>
26
+ `hsl(var(${name}, var(${fallback})) / <alpha-value>)`
20
27
 
21
28
  const preset = {
22
29
  theme: {
@@ -41,6 +48,29 @@ const preset = {
41
48
  hover: hsl('--color-accent-hover'),
42
49
  dim: hsl('--color-accent-dim'),
43
50
  },
51
+
52
+ /**
53
+ * Solid CTA fill. Defaults to accent when `--btn-primary-*` are unset
54
+ * (QVAC) or alias accent (WDK/MDK).
55
+ */
56
+ 'btn': {
57
+ DEFAULT: hslFallback('--btn-primary-bg', '--color-accent'),
58
+ hover: hslFallback('--btn-primary-bg-hover', '--color-accent-hover'),
59
+ foreground: hslFallback('--btn-primary-text', '--color-text-primary'),
60
+ },
61
+ /** Outline CTA border/text. Defaults to accent border + primary text. */
62
+ 'btn-secondary': {
63
+ border: hslFallback('--btn-secondary-border', '--color-accent'),
64
+ DEFAULT: hslFallback('--btn-secondary-text', '--color-text-primary'),
65
+ },
66
+ /**
67
+ * Form control stroke. Defaults to border-subtle (current Input/Textarea).
68
+ * Set `--control-border` to remap without CSS attribute overrides.
69
+ */
70
+ 'control': {
71
+ border: hslFallback('--control-border', '--color-border-subtle'),
72
+ },
73
+
44
74
  'border': {
45
75
  DEFAULT: hsl('--color-border-default'),
46
76
  subtle: hsl('--color-border-subtle'),
@@ -104,6 +134,10 @@ const preset = {
104
134
  },
105
135
 
106
136
  borderRadius: {
137
+ // Keep 5px default (current WDK/MDK render). Apps override via
138
+ // theme.extend.borderRadius.btn (QVAC / Pear) — do not read
139
+ // `--btn-primary-radius` here so WDK’s declared 4px token does not
140
+ // silently change live buttons from 5px → 4px.
107
141
  btn: '5px',
108
142
  card: '10px',
109
143
  badge: '5px',