velloo 0.0.0 → 0.2.0
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/BUN-LICENSE.md +91 -0
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +288 -2
- package/THIRD-PARTY-NOTICES.md +467 -0
- package/assets/elsewhere-alps-9fqhv029.jpg +0 -0
- package/assets/elsewhere-bali-vp60g115.jpg +0 -0
- package/assets/elsewhere-forest-xgn23zrj.jpg +0 -0
- package/assets/elsewhere-japan-qs7qkbr2.jpg +0 -0
- package/assets/elsewhere-kyoto-map-n2bvkrya.svg +1 -0
- package/assets/elsewhere-kyoto-qemtn6nk.jpg +0 -0
- package/assets/elsewhere-room-g1gaw7cs.jpg +0 -0
- package/canvas/apple-touch-icon.png +0 -0
- package/canvas/assets/index-BCvDZiBo.css +2 -0
- package/canvas/assets/index-ClaBqcaS.js +28 -0
- package/canvas/assets/lucide-all-iIF-pmzN.js +1 -0
- package/canvas/assets/radix-C3HaOB-d.js +41 -0
- package/canvas/assets/react-D0Ec-185.js +9 -0
- package/canvas/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/canvas/assets/vendor-CKmQ-Zst.js +53 -0
- package/canvas/favicon.ico +0 -0
- package/canvas/favicon.svg +12 -0
- package/canvas/fonts/OFL-1.1.txt +93 -0
- package/canvas/fonts/poppins-700.woff2 +0 -0
- package/canvas/icon-192.png +0 -0
- package/canvas/icon-512.png +0 -0
- package/canvas/index.html +22 -0
- package/canvas/site.webmanifest +11 -0
- package/chunk-08sv3k41.js +4 -0
- package/chunk-0fe83yzt.js +2 -0
- package/chunk-0t68szgr.js +3 -0
- package/chunk-1h55s5he.js +2 -0
- package/chunk-2c7jbx46.js +2 -0
- package/chunk-2smsg2ct.js +7 -0
- package/chunk-308ynsxd.js +2 -0
- package/chunk-3e8vez4d.js +1229 -0
- package/chunk-40xgcth8.js +63 -0
- package/chunk-4dn75m6t.js +3 -0
- package/chunk-4fj5tahv.js +3 -0
- package/chunk-6nqghjda.js +3 -0
- package/chunk-6tx68d90.js +3 -0
- package/chunk-6w6ch4ce.js +2 -0
- package/chunk-735ezz23.js +86 -0
- package/chunk-7eq7nx37.js +7 -0
- package/chunk-7eyb8yrf.js +6 -0
- package/chunk-80fjfyha.js +86 -0
- package/chunk-80r93t2e.js +4 -0
- package/chunk-94vx1yb0.js +3 -0
- package/chunk-9vg3yafz.js +3 -0
- package/chunk-a84n72m2.js +4 -0
- package/chunk-ampmdzz4.js +10 -0
- package/chunk-avns4dbf.js +2 -0
- package/chunk-ayrb3qnm.js +3 -0
- package/chunk-bwnvcncx.js +3 -0
- package/chunk-ccszg02q.js +5 -0
- package/chunk-cvn689d5.js +3 -0
- package/chunk-cxe6jjwj.js +3 -0
- package/chunk-dg6z4vdd.js +3 -0
- package/chunk-dgwjpbp9.js +3 -0
- package/chunk-dy41n60j.js +52 -0
- package/chunk-ek71z2fv.js +226 -0
- package/chunk-er1fxb7d.js +2 -0
- package/chunk-eyr8hrzf.js +2 -0
- package/chunk-fjbzk4sw.js +3 -0
- package/chunk-fsbsksga.js +3 -0
- package/chunk-h6gchare.js +3 -0
- package/chunk-hcb68zt7.js +85 -0
- package/chunk-hggap8fq.js +3 -0
- package/chunk-j11fyeqh.js +2 -0
- package/chunk-jb21jkp1.js +8 -0
- package/chunk-jfx9xjmr.js +18 -0
- package/chunk-jr0ss0mm.js +303 -0
- package/chunk-jrsknzjt.js +3 -0
- package/chunk-jv0xggh0.js +2 -0
- package/chunk-k17gfwq3.js +2 -0
- package/chunk-mh2pr7cq.js +5 -0
- package/chunk-mnapdp1y.js +2 -0
- package/chunk-mzhdrawd.js +3 -0
- package/chunk-n9gs6yx9.js +2 -0
- package/chunk-ngnymg2q.js +3 -0
- package/chunk-nwx5700b.js +3 -0
- package/chunk-pjtg3wnm.js +4 -0
- package/chunk-pp86v6m6.js +3 -0
- package/chunk-r22epxz2.js +3 -0
- package/chunk-rgasy862.js +5 -0
- package/chunk-rpwpeph5.js +3 -0
- package/chunk-s23gtejf.js +3 -0
- package/chunk-sjgqnykd.js +6 -0
- package/chunk-sn3x2m0m.js +6 -0
- package/chunk-t448rrfr.js +2 -0
- package/chunk-tdsk5wxa.js +451 -0
- package/chunk-v40fn08f.js +442 -0
- package/chunk-vbk1qhdz.js +3 -0
- package/chunk-vdkrbzh1.js +2 -0
- package/chunk-vgmr98cv.js +40 -0
- package/chunk-vsv8z729.js +6 -0
- package/chunk-x4f4k9pp.js +26 -0
- package/chunk-xt5ayg7g.js +4 -0
- package/chunk-ydhwv29b.js +244 -0
- package/chunk-zb6hc1y0.js +2 -0
- package/cli.js +3 -0
- package/launcher.cjs +97 -0
- package/package.json +58 -5
- package/pkgs/helpers/src/box.tsx +21 -0
- package/pkgs/helpers/src/cn.ts +32 -0
- package/pkgs/helpers/src/descriptors.ts +270 -0
- package/pkgs/helpers/src/divider.tsx +123 -0
- package/pkgs/helpers/src/gradient.tsx +90 -0
- package/pkgs/helpers/src/heading.tsx +20 -0
- package/pkgs/helpers/src/icon-data.ts +8009 -0
- package/pkgs/helpers/src/icon.tsx +53 -0
- package/pkgs/helpers/src/image.tsx +111 -0
- package/pkgs/helpers/src/index.ts +29 -0
- package/pkgs/helpers/src/layer.tsx +61 -0
- package/pkgs/helpers/src/lowering.ts +53 -0
- package/pkgs/helpers/src/paths.ts +47 -0
- package/pkgs/helpers/src/placeholder.tsx +88 -0
- package/pkgs/helpers/src/prose.tsx +42 -0
- package/pkgs/helpers/src/registry.ts +41 -0
- package/pkgs/helpers/src/svg.tsx +62 -0
- package/pkgs/helpers/src/text.tsx +16 -0
- package/pkgs/provider-antd/src/index.ts +78 -0
- package/pkgs/provider-antd/src/intro.ts +14 -0
- package/pkgs/provider-antd/src/manifest.ts +439 -0
- package/pkgs/provider-antd/src/overlays.ts +176 -0
- package/pkgs/provider-antd/src/registry.ts +104 -0
- package/pkgs/provider-antd/src/render-pass.ts +43 -0
- package/pkgs/provider-antd/src/tailwind-entry.css +21 -0
- package/pkgs/provider-antd/src/theme.ts +132 -0
- package/pkgs/provider-antd/src/version.ts +8 -0
- package/pkgs/provider-chakra/src/index.ts +80 -0
- package/pkgs/provider-chakra/src/intro.ts +14 -0
- package/pkgs/provider-chakra/src/manifest.ts +421 -0
- package/pkgs/provider-chakra/src/overlays.ts +246 -0
- package/pkgs/provider-chakra/src/registry.ts +227 -0
- package/pkgs/provider-chakra/src/render-pass.ts +55 -0
- package/pkgs/provider-chakra/src/tailwind-entry.css +21 -0
- package/pkgs/provider-chakra/src/theme.ts +180 -0
- package/pkgs/provider-chakra/src/version.ts +8 -0
- package/pkgs/provider-mui/src/index.ts +117 -0
- package/pkgs/provider-mui/src/intro.ts +12 -0
- package/pkgs/provider-mui/src/manifest.ts +263 -0
- package/pkgs/provider-mui/src/overlays.ts +124 -0
- package/pkgs/provider-mui/src/registry.ts +120 -0
- package/pkgs/provider-mui/src/render-pass.ts +36 -0
- package/pkgs/provider-mui/src/tailwind-entry.css +21 -0
- package/pkgs/provider-mui/src/theme.ts +129 -0
- package/pkgs/provider-mui/src/version.ts +8 -0
- package/pkgs/provider-none/src/components-inline.tsx +204 -0
- package/pkgs/provider-none/src/components.tsx +155 -0
- package/pkgs/provider-none/src/index.ts +57 -0
- package/pkgs/provider-none/src/intro.ts +18 -0
- package/pkgs/provider-none/src/manifest.ts +163 -0
- package/pkgs/provider-none/src/registry-inline.ts +36 -0
- package/pkgs/provider-none/src/registry.ts +39 -0
- package/pkgs/provider-none/src/tailwind-entry.css +57 -0
- package/pkgs/provider-none/src/version.ts +10 -0
- package/pkgs/schema/src/annotation.ts +120 -0
- package/pkgs/schema/src/asset.ts +66 -0
- package/pkgs/schema/src/board.ts +71 -0
- package/pkgs/schema/src/comment.ts +107 -0
- package/pkgs/schema/src/config.ts +185 -0
- package/pkgs/schema/src/css-sanitize.ts +56 -0
- package/pkgs/schema/src/extension.ts +105 -0
- package/pkgs/schema/src/fonts.ts +690 -0
- package/pkgs/schema/src/frame.ts +40 -0
- package/pkgs/schema/src/icon-name.ts +21 -0
- package/pkgs/schema/src/ids.ts +14 -0
- package/pkgs/schema/src/index.ts +169 -0
- package/pkgs/schema/src/migrate.ts +297 -0
- package/pkgs/schema/src/node.ts +229 -0
- package/pkgs/schema/src/paths.ts +28 -0
- package/pkgs/schema/src/repo.ts +134 -0
- package/pkgs/schema/src/screen.ts +30 -0
- package/pkgs/schema/src/snippet-resolve.ts +193 -0
- package/pkgs/schema/src/snippet.ts +104 -0
- package/pkgs/schema/src/svg-sanitize.ts +487 -0
- package/pkgs/schema/src/theme.ts +218 -0
- package/pkgs/schema/src/typeset.ts +693 -0
- package/pkgs/schema/src/validate-ids.ts +46 -0
- package/pkgs/schema/src/viewport.ts +8 -0
- package/pkgs/shadcn-snapshot/dist/manifest.json +8109 -0
- package/pkgs/shadcn-snapshot/src/components/canvas-portal.tsx +55 -0
- package/pkgs/shadcn-snapshot/src/components/ui/accordion.tsx +122 -0
- package/pkgs/shadcn-snapshot/src/components/ui/alert-dialog.tsx +164 -0
- package/pkgs/shadcn-snapshot/src/components/ui/alert.tsx +74 -0
- package/pkgs/shadcn-snapshot/src/components/ui/aspect-ratio.tsx +13 -0
- package/pkgs/shadcn-snapshot/src/components/ui/attachment.tsx +197 -0
- package/pkgs/shadcn-snapshot/src/components/ui/avatar.tsx +100 -0
- package/pkgs/shadcn-snapshot/src/components/ui/badge.tsx +50 -0
- package/pkgs/shadcn-snapshot/src/components/ui/breadcrumb.tsx +109 -0
- package/pkgs/shadcn-snapshot/src/components/ui/bubble.tsx +130 -0
- package/pkgs/shadcn-snapshot/src/components/ui/button-group.tsx +82 -0
- package/pkgs/shadcn-snapshot/src/components/ui/button.tsx +72 -0
- package/pkgs/shadcn-snapshot/src/components/ui/calendar.tsx +229 -0
- package/pkgs/shadcn-snapshot/src/components/ui/card.tsx +92 -0
- package/pkgs/shadcn-snapshot/src/components/ui/carousel.tsx +235 -0
- package/pkgs/shadcn-snapshot/src/components/ui/chart-option.ts +250 -0
- package/pkgs/shadcn-snapshot/src/components/ui/chart.tsx +177 -0
- package/pkgs/shadcn-snapshot/src/components/ui/checkbox.tsx +41 -0
- package/pkgs/shadcn-snapshot/src/components/ui/collapsible.tsx +32 -0
- package/pkgs/shadcn-snapshot/src/components/ui/combobox.tsx +196 -0
- package/pkgs/shadcn-snapshot/src/components/ui/context-menu.tsx +227 -0
- package/pkgs/shadcn-snapshot/src/components/ui/dialog.tsx +131 -0
- package/pkgs/shadcn-snapshot/src/components/ui/direction.tsx +24 -0
- package/pkgs/shadcn-snapshot/src/components/ui/drawer.tsx +112 -0
- package/pkgs/shadcn-snapshot/src/components/ui/dropdown-menu.tsx +205 -0
- package/pkgs/shadcn-snapshot/src/components/ui/empty.tsx +98 -0
- package/pkgs/shadcn-snapshot/src/components/ui/field.tsx +228 -0
- package/pkgs/shadcn-snapshot/src/components/ui/hover-card.tsx +51 -0
- package/pkgs/shadcn-snapshot/src/components/ui/input-group.tsx +148 -0
- package/pkgs/shadcn-snapshot/src/components/ui/input.tsx +43 -0
- package/pkgs/shadcn-snapshot/src/components/ui/item.tsx +187 -0
- package/pkgs/shadcn-snapshot/src/components/ui/kbd.tsx +30 -0
- package/pkgs/shadcn-snapshot/src/components/ui/label.tsx +25 -0
- package/pkgs/shadcn-snapshot/src/components/ui/marker.tsx +71 -0
- package/pkgs/shadcn-snapshot/src/components/ui/menubar.tsx +243 -0
- package/pkgs/shadcn-snapshot/src/components/ui/message.tsx +89 -0
- package/pkgs/shadcn-snapshot/src/components/ui/native-select.tsx +60 -0
- package/pkgs/shadcn-snapshot/src/components/ui/navigation-menu.tsx +165 -0
- package/pkgs/shadcn-snapshot/src/components/ui/pagination.tsx +117 -0
- package/pkgs/shadcn-snapshot/src/components/ui/popover.tsx +66 -0
- package/pkgs/shadcn-snapshot/src/components/ui/progress.tsx +35 -0
- package/pkgs/shadcn-snapshot/src/components/ui/radio-group.tsx +48 -0
- package/pkgs/shadcn-snapshot/src/components/ui/scroll-area.tsx +59 -0
- package/pkgs/shadcn-snapshot/src/components/ui/select.tsx +180 -0
- package/pkgs/shadcn-snapshot/src/components/ui/separator.tsx +32 -0
- package/pkgs/shadcn-snapshot/src/components/ui/sheet.tsx +107 -0
- package/pkgs/shadcn-snapshot/src/components/ui/skeleton.tsx +17 -0
- package/pkgs/shadcn-snapshot/src/components/ui/slider.tsx +68 -0
- package/pkgs/shadcn-snapshot/src/components/ui/sonner.tsx +91 -0
- package/pkgs/shadcn-snapshot/src/components/ui/spinner.tsx +21 -0
- package/pkgs/shadcn-snapshot/src/components/ui/switch.tsx +41 -0
- package/pkgs/shadcn-snapshot/src/components/ui/table.tsx +93 -0
- package/pkgs/shadcn-snapshot/src/components/ui/tabs.tsx +84 -0
- package/pkgs/shadcn-snapshot/src/components/ui/textarea.tsx +24 -0
- package/pkgs/shadcn-snapshot/src/components/ui/toggle-group.tsx +94 -0
- package/pkgs/shadcn-snapshot/src/components/ui/toggle.tsx +50 -0
- package/pkgs/shadcn-snapshot/src/components/ui/tooltip.tsx +47 -0
- package/pkgs/shadcn-snapshot/src/examples.ts +36 -0
- package/pkgs/shadcn-snapshot/src/groups.ts +87 -0
- package/pkgs/shadcn-snapshot/src/index.ts +43 -0
- package/pkgs/shadcn-snapshot/src/lib/utils.ts +4 -0
- package/pkgs/shadcn-snapshot/src/manifest.ts +12 -0
- package/pkgs/shadcn-snapshot/src/notes.ts +97 -0
- package/pkgs/shadcn-snapshot/src/paths.ts +47 -0
- package/pkgs/shadcn-snapshot/src/registry-files.ts +293 -0
- package/pkgs/shadcn-snapshot/src/registry.ts +631 -0
- package/pkgs/shadcn-snapshot/src/shadcn-tailwind.css +632 -0
- package/pkgs/shadcn-snapshot/src/tailwind-entry.css +159 -0
- package/pkgs/shadcn-snapshot/src/version.ts +8 -0
- package/plugins/claude/velloo/.claude-plugin/plugin.json +6 -0
- package/plugins/claude/velloo/agents/velloo-design-reviewer.md +33 -0
- package/plugins/claude/velloo/agents/velloo-designer.md +39 -0
- package/plugins/claude/velloo/commands/design.md +11 -0
- package/plugins/claude/velloo/commands/implement.md +10 -0
- package/plugins/claude/velloo/commands/review.md +8 -0
- package/plugins/gemini/GEMINI.md +31 -0
- package/plugins/gemini/commands/velloo/design.toml +10 -0
- package/plugins/gemini/commands/velloo/implement.toml +9 -0
- package/plugins/gemini/gemini-extension.json +9 -0
- package/skills/velloo-brand/SKILL.md +124 -0
- package/skills/velloo-design/SKILL.md +144 -0
- package/skills/velloo-design-system/SKILL.md +90 -0
- package/skills/velloo-implement/SKILL.md +101 -0
- package/skills/velloo-logo/SKILL.md +93 -0
- package/skills/velloo-setup/SKILL.md +156 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typeset derivation — velloo's one home for typographic proportion.
|
|
3
|
+
*
|
|
4
|
+
* A typeset is three rhythm controls (size / leading / flow) plus font roles.
|
|
5
|
+
* Everything else — the h1..h6 ladder, body/lead/small copy, the space under a
|
|
6
|
+
* heading, list indents — derives from them through `TYPESET_RATIOS`. An agent
|
|
7
|
+
* reasons about three knobs far more reliably than a twelve-entry scale, and one
|
|
8
|
+
* ratio table means the canvas, the emitted CSS, the native framework themes,
|
|
9
|
+
* and the Heading/Text components cannot drift apart.
|
|
10
|
+
*
|
|
11
|
+
* Deliberately zod-free and exported as `@velloo/schema/typeset` so the canvas,
|
|
12
|
+
* the helpers, and the providers can import the tables without pulling zod into
|
|
13
|
+
* a browser bundle. The Zod shape lives in `theme.ts` and re-uses these types.
|
|
14
|
+
*
|
|
15
|
+
* Two output modes over the same ratios:
|
|
16
|
+
* - `typesetCss` / `typesetVars` — CSS custom properties, for every channel
|
|
17
|
+
* that renders through a stylesheet (Tailwind utilities, inline `style`, the
|
|
18
|
+
* `:where()` element rules). Values stay symbolic, so a rhythm change
|
|
19
|
+
* re-flows every frame without re-rendering.
|
|
20
|
+
* - `typesetScale` — the same ratios resolved to concrete numbers, for the
|
|
21
|
+
* native framework themes (MUI/antd/chakra), whose theme objects get
|
|
22
|
+
* serialized into codegen artifacts where no CSS variables exist.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { isCssIdent, sanitizeCssTokenValue } from "./css-sanitize.ts";
|
|
26
|
+
|
|
27
|
+
/** The three rhythm controls plus font roles. Every field is optional; `TYPESET_DEFAULT` fills the gaps. */
|
|
28
|
+
export interface Typeset {
|
|
29
|
+
/** Base text size. `1em` follows the surrounding layout (the container-relative default). */
|
|
30
|
+
size?: string | number | undefined;
|
|
31
|
+
/** Body line-height, unitless. Heading leading derives from it. */
|
|
32
|
+
leading?: number | undefined;
|
|
33
|
+
/** Vertical space between blocks. Heading margins and rule spacing derive from it. */
|
|
34
|
+
flow?: string | number | undefined;
|
|
35
|
+
/** Font *role* name (a key of `typography.fontFamily`) for body copy. */
|
|
36
|
+
fontBody?: string | undefined;
|
|
37
|
+
/** Font role name for headings. */
|
|
38
|
+
fontHeading?: string | undefined;
|
|
39
|
+
/** Font role name for code. */
|
|
40
|
+
fontMono?: string | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Baseline rhythm — shadcn/typeset's defaults, which read well on long-form
|
|
45
|
+
* content.
|
|
46
|
+
*
|
|
47
|
+
* Spelled out rather than `Required<Pick<Typeset, …>>`: `Required` strips the
|
|
48
|
+
* `?` but not an explicit `| undefined`, which `Typeset`'s fields carry so
|
|
49
|
+
* they can hold zod's output. The defaults are concrete, so say so.
|
|
50
|
+
*/
|
|
51
|
+
export const TYPESET_DEFAULT: { size: string | number; leading: number; flow: string | number } = {
|
|
52
|
+
size: "1em",
|
|
53
|
+
leading: 1.75,
|
|
54
|
+
flow: "1.25em",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** The name of the typeset every folder has: the baseline projected onto `:root`. */
|
|
58
|
+
export const DEFAULT_TYPESET_NAME = "default";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Typeset names are emitted as `.typeset-<name>` class selectors, so they must be
|
|
62
|
+
* safe idents. `default` is reserved for the folder baseline (it lands on
|
|
63
|
+
* `:root` and on `.typeset`, not on a preset class).
|
|
64
|
+
*/
|
|
65
|
+
export function isTypesetName(name: string): boolean {
|
|
66
|
+
return isCssIdent(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Roles in the derived scale. Fixed vocabulary — the *values* vary per typeset,
|
|
71
|
+
* the names never do, which is what lets the class literals live in a shared
|
|
72
|
+
* module and be safelisted rather than scanned.
|
|
73
|
+
*
|
|
74
|
+
* Names are chosen to stay clear of the theme's other Tailwind namespaces: a
|
|
75
|
+
* size token becomes a `text-<role>` utility, which would collide with the
|
|
76
|
+
* `text-<color>` utility that `--color-<name>` generates. Hence `caption` rather
|
|
77
|
+
* than `muted` — `muted` is a semantic color slot in every velloo theme.
|
|
78
|
+
*/
|
|
79
|
+
export type TypesetRole =
|
|
80
|
+
| "h1"
|
|
81
|
+
| "h2"
|
|
82
|
+
| "h3"
|
|
83
|
+
| "h4"
|
|
84
|
+
| "h5"
|
|
85
|
+
| "h6"
|
|
86
|
+
| "body"
|
|
87
|
+
| "lead"
|
|
88
|
+
| "small"
|
|
89
|
+
| "caption";
|
|
90
|
+
|
|
91
|
+
export interface TypesetRatio {
|
|
92
|
+
/** Multiplier on the typeset's base size. */
|
|
93
|
+
size: number;
|
|
94
|
+
/** Multiplier on the typeset's leading — so a leading change moves the whole ladder proportionally. */
|
|
95
|
+
leading: number;
|
|
96
|
+
/** Letter-spacing, in `em` of the role's own size. Optical, so it does not scale with rhythm. */
|
|
97
|
+
tracking: string;
|
|
98
|
+
/** Font weight. Constant per role: weight is deliberately not one of the three controls. */
|
|
99
|
+
weight: 400 | 500 | 600 | 700;
|
|
100
|
+
/** Which font role paints this — headings take the heading face, copy takes the body face. */
|
|
101
|
+
face: "heading" | "body";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The ratio table. **The only place typographic proportion is written down.**
|
|
106
|
+
* `typesetVars`, `typesetCss`, and `typesetScale` all read it, which is what
|
|
107
|
+
* replaced four hand-synced class tables (helpers' Tailwind ladder, its codegen
|
|
108
|
+
* mirror, provider-none's inline ladder, and that ladder's codegen mirror).
|
|
109
|
+
*/
|
|
110
|
+
export const TYPESET_RATIOS: Record<TypesetRole, TypesetRatio> = {
|
|
111
|
+
h1: { size: 2.5, leading: 0.63, tracking: "-0.025em", weight: 700, face: "heading" },
|
|
112
|
+
h2: { size: 2, leading: 0.66, tracking: "-0.025em", weight: 700, face: "heading" },
|
|
113
|
+
h3: { size: 1.5, leading: 0.71, tracking: "-0.02em", weight: 600, face: "heading" },
|
|
114
|
+
h4: { size: 1.25, leading: 0.74, tracking: "-0.02em", weight: 600, face: "heading" },
|
|
115
|
+
h5: { size: 1.125, leading: 0.8, tracking: "-0.015em", weight: 600, face: "heading" },
|
|
116
|
+
h6: { size: 1, leading: 0.8, tracking: "-0.01em", weight: 600, face: "heading" },
|
|
117
|
+
body: { size: 1, leading: 1, tracking: "0em", weight: 400, face: "body" },
|
|
118
|
+
lead: { size: 1.25, leading: 0.91, tracking: "0em", weight: 400, face: "body" },
|
|
119
|
+
small: { size: 0.875, leading: 0.57, tracking: "0em", weight: 500, face: "body" },
|
|
120
|
+
caption: { size: 0.875, leading: 0.86, tracking: "0em", weight: 400, face: "body" },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Every role name, in ladder order. Drives the `@theme` tokens, the Tailwind safelist, and the twMerge config. */
|
|
124
|
+
export const TYPESET_SCALE_NAMES: readonly TypesetRole[] = Object.keys(
|
|
125
|
+
TYPESET_RATIOS,
|
|
126
|
+
) as TypesetRole[];
|
|
127
|
+
|
|
128
|
+
/** Heading level → scale role. */
|
|
129
|
+
export const HEADING_ROLE_BY_LEVEL: Record<number, TypesetRole> = {
|
|
130
|
+
1: "h1",
|
|
131
|
+
2: "h2",
|
|
132
|
+
3: "h3",
|
|
133
|
+
4: "h4",
|
|
134
|
+
5: "h5",
|
|
135
|
+
6: "h6",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** The `Text` helper's visual variants. */
|
|
139
|
+
export type TextVariant = "default" | "muted" | "small" | "lead";
|
|
140
|
+
|
|
141
|
+
/** Text variant → scale role. */
|
|
142
|
+
export const TEXT_ROLE_BY_VARIANT: Record<TextVariant, TypesetRole> = {
|
|
143
|
+
default: "body",
|
|
144
|
+
muted: "caption",
|
|
145
|
+
small: "small",
|
|
146
|
+
lead: "lead",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The semantic color slot each text variant paints with, or absent to inherit.
|
|
151
|
+
* Colour is not part of the typeset — it lives here only so the four channels
|
|
152
|
+
* that lower `Text` (Tailwind runtime + codegen, inline runtime + codegen) read
|
|
153
|
+
* one table instead of four.
|
|
154
|
+
*/
|
|
155
|
+
export const TEXT_TONE_BY_VARIANT: Partial<Record<TextVariant, string>> = {
|
|
156
|
+
default: "foreground",
|
|
157
|
+
muted: "muted-foreground",
|
|
158
|
+
lead: "muted-foreground",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Stock Tailwind weight utility per weight value. */
|
|
162
|
+
const WEIGHT_CLASS: Record<TypesetRatio["weight"], string> = {
|
|
163
|
+
400: "font-normal",
|
|
164
|
+
500: "font-medium",
|
|
165
|
+
600: "font-semibold",
|
|
166
|
+
700: "font-bold",
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The Tailwind utilities each role resolves to. Fixed literal class names whose
|
|
171
|
+
* *values* come from the theme's generated `@theme` tokens — which is what lets
|
|
172
|
+
* these strings live in a plain `.ts` module: the JIT learns them from the
|
|
173
|
+
* generated `@source inline(...)` safelist rather than from scanning a `.tsx`.
|
|
174
|
+
*
|
|
175
|
+
* Weight uses the stock utility rather than a themed token: weight is constant
|
|
176
|
+
* per role, and a `--font-weight-<role>` token would generate a `font-<role>`
|
|
177
|
+
* utility that collides with the `font-<role>` a font *family* declares.
|
|
178
|
+
*/
|
|
179
|
+
export const TYPESET_CLASSES: Record<TypesetRole, string> = Object.fromEntries(
|
|
180
|
+
TYPESET_SCALE_NAMES.map((role) => {
|
|
181
|
+
const ratio = TYPESET_RATIOS[role];
|
|
182
|
+
return [
|
|
183
|
+
role,
|
|
184
|
+
`text-${role} leading-${role} tracking-${role} ${WEIGHT_CLASS[ratio.weight]}`,
|
|
185
|
+
] as const;
|
|
186
|
+
}),
|
|
187
|
+
) as Record<TypesetRole, string>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The inline `style` object each role resolves to, for the `none`-CSS channel.
|
|
191
|
+
* Sizes are `var()` references into the same derived tokens the utilities read,
|
|
192
|
+
* so a `none/none` folder gets themed type and re-rhythms live like every other
|
|
193
|
+
* channel; weight is a literal because it never varies with rhythm.
|
|
194
|
+
*/
|
|
195
|
+
export const TYPESET_INLINE_STYLE: Record<
|
|
196
|
+
TypesetRole,
|
|
197
|
+
{ fontSize: string; lineHeight: string; letterSpacing: string; fontWeight: number }
|
|
198
|
+
> = Object.fromEntries(
|
|
199
|
+
TYPESET_SCALE_NAMES.map((role) => [
|
|
200
|
+
role,
|
|
201
|
+
{
|
|
202
|
+
fontSize: `var(--text-${role})`,
|
|
203
|
+
lineHeight: `var(--leading-${role})`,
|
|
204
|
+
letterSpacing: `var(--tracking-${role})`,
|
|
205
|
+
fontWeight: TYPESET_RATIOS[role].weight,
|
|
206
|
+
},
|
|
207
|
+
]),
|
|
208
|
+
) as Record<
|
|
209
|
+
TypesetRole,
|
|
210
|
+
{ fontSize: string; lineHeight: string; letterSpacing: string; fontWeight: number }
|
|
211
|
+
>;
|
|
212
|
+
|
|
213
|
+
/** Clamp an authored heading level to 1–6, so the tag and the ladder rung always agree. */
|
|
214
|
+
export function resolveHeadingLevel(level: unknown): number {
|
|
215
|
+
const n = Number(level ?? 1);
|
|
216
|
+
return Number.isFinite(n) && n >= 1 && n <= 6 ? Math.trunc(n) : 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function textVariant(variant: unknown): TextVariant {
|
|
220
|
+
const v = String(variant ?? "default");
|
|
221
|
+
return v in TEXT_ROLE_BY_VARIANT ? (v as TextVariant) : "default";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The Tailwind classes a `Heading` of this level renders with. THE definition —
|
|
226
|
+
* the runtime component and the codegen lowering both call this, which is what
|
|
227
|
+
* retired the hand-synced `lowering.ts` mirror.
|
|
228
|
+
*/
|
|
229
|
+
export function headingClasses(level: unknown): string {
|
|
230
|
+
const role = HEADING_ROLE_BY_LEVEL[resolveHeadingLevel(level)] as TypesetRole;
|
|
231
|
+
return TYPESET_CLASSES[role];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The Tailwind classes a `Text` of this variant renders with, tone included. */
|
|
235
|
+
export function textClasses(variant: unknown): string {
|
|
236
|
+
const v = textVariant(variant);
|
|
237
|
+
const tone = TEXT_TONE_BY_VARIANT[v];
|
|
238
|
+
return tone
|
|
239
|
+
? `${TYPESET_CLASSES[TEXT_ROLE_BY_VARIANT[v]]} text-${tone}`
|
|
240
|
+
: TYPESET_CLASSES[TEXT_ROLE_BY_VARIANT[v]];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The inline `style` a `Heading` of this level renders with, on the `none`-CSS channel. */
|
|
244
|
+
export function headingInlineStyle(level: unknown): Record<string, string | number> {
|
|
245
|
+
const role = HEADING_ROLE_BY_LEVEL[resolveHeadingLevel(level)] as TypesetRole;
|
|
246
|
+
return { ...TYPESET_INLINE_STYLE[role] };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The inline `style` a `Text` of this variant renders with, on the `none`-CSS channel. */
|
|
250
|
+
export function textInlineStyle(variant: unknown): Record<string, string | number> {
|
|
251
|
+
const v = textVariant(variant);
|
|
252
|
+
const tone = TEXT_TONE_BY_VARIANT[v];
|
|
253
|
+
return {
|
|
254
|
+
...TYPESET_INLINE_STYLE[TEXT_ROLE_BY_VARIANT[v]],
|
|
255
|
+
...(tone ? { color: `var(--color-${tone})` } : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* `@source inline(...)` arguments covering every utility the scale can produce.
|
|
261
|
+
* Emitted with the generated `@theme` block so the utilities compile whether or
|
|
262
|
+
* not a class literal happens to be scanned — the mechanism that let the ladder
|
|
263
|
+
* move out of `heading.tsx` into a shared module.
|
|
264
|
+
*/
|
|
265
|
+
export function typesetSafelist(): string[] {
|
|
266
|
+
const roles = TYPESET_SCALE_NAMES.join(",");
|
|
267
|
+
const weights = Object.values(WEIGHT_CLASS)
|
|
268
|
+
.map((c) => c.replace(/^font-/, ""))
|
|
269
|
+
.join(",");
|
|
270
|
+
// The tone classes ride along: `textClasses` composes them from the same
|
|
271
|
+
// unscanned module, so they need the same treatment as the scale itself.
|
|
272
|
+
const tones = [...new Set(Object.values(TEXT_TONE_BY_VARIANT))].join(",");
|
|
273
|
+
return [`{text,leading,tracking}-{${roles}}`, `font-{${weights}}`, `text-{${tones}}`];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Every utility class the scale can produce, deduped. Tailwind v3 has no
|
|
278
|
+
* `@source inline(...)`, so its emitted preset safelists this list instead.
|
|
279
|
+
*/
|
|
280
|
+
export function typesetUtilityClasses(): string[] {
|
|
281
|
+
const seen = new Set<string>();
|
|
282
|
+
for (const classes of Object.values(TYPESET_CLASSES)) {
|
|
283
|
+
for (const cls of classes.split(" ")) seen.add(cls);
|
|
284
|
+
}
|
|
285
|
+
for (const tone of Object.values(TEXT_TONE_BY_VARIANT)) seen.add(`text-${tone}`);
|
|
286
|
+
return [...seen];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The scale as a Tailwind v3 `theme.extend.fontSize` map. Each entry carries its
|
|
291
|
+
* paired leading and tracking, and every value is a `var()` into the typeset
|
|
292
|
+
* sheet — so a v3 app re-derives inside a `.typeset-<preset>` region exactly
|
|
293
|
+
* like a v4 one.
|
|
294
|
+
*/
|
|
295
|
+
export function typesetV3FontSize(): Record<
|
|
296
|
+
string,
|
|
297
|
+
[string, { lineHeight: string; letterSpacing: string }]
|
|
298
|
+
> {
|
|
299
|
+
return Object.fromEntries(
|
|
300
|
+
TYPESET_SCALE_NAMES.map((role) => [
|
|
301
|
+
role,
|
|
302
|
+
[
|
|
303
|
+
`var(--text-${role})`,
|
|
304
|
+
{ lineHeight: `var(--leading-${role})`, letterSpacing: `var(--tracking-${role})` },
|
|
305
|
+
],
|
|
306
|
+
]),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Placeholder `@theme` tokens so `text-<role>` / `leading-<role>` /
|
|
312
|
+
* `tracking-<role>` exist as utilities. Values here are irrelevant — the JIT
|
|
313
|
+
* only needs the token to be declared, and `typesetCss` overrides every one of
|
|
314
|
+
* them at render time (same contract as the `--font-<role>` tokens).
|
|
315
|
+
*/
|
|
316
|
+
export function typesetThemeTokens(): string[] {
|
|
317
|
+
const lines: string[] = [];
|
|
318
|
+
for (const role of TYPESET_SCALE_NAMES) {
|
|
319
|
+
const ratio = TYPESET_RATIOS[role];
|
|
320
|
+
lines.push(` --text-${role}: ${ratio.size}rem;`);
|
|
321
|
+
lines.push(` --leading-${role}: ${round(ratio.leading * TYPESET_DEFAULT.leading)};`);
|
|
322
|
+
lines.push(` --tracking-${role}: ${ratio.tracking};`);
|
|
323
|
+
}
|
|
324
|
+
return lines;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function round(n: number): number {
|
|
328
|
+
return Math.round(n * 1000) / 1000;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** A length authored as a bare number means px, matching the rest of the theme's token handling. */
|
|
332
|
+
function len(value: string | number): string {
|
|
333
|
+
return typeof value === "number" ? `${value}px` : sanitizeCssTokenValue(value);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Resolve a font *role* name to the `var(--font-<role>)` reference, or a fallback when unset/unsafe. */
|
|
337
|
+
function faceVar(role: string | undefined, fallback: string): string {
|
|
338
|
+
if (!role || !isCssIdent(role)) return fallback;
|
|
339
|
+
return `var(--font-${role})`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Fill a partial typeset from the baseline. */
|
|
343
|
+
export function resolveTypeset(typeset: Typeset | undefined): Typeset & {
|
|
344
|
+
size: string | number;
|
|
345
|
+
leading: number;
|
|
346
|
+
flow: string | number;
|
|
347
|
+
} {
|
|
348
|
+
return {
|
|
349
|
+
...typeset,
|
|
350
|
+
size: typeset?.size ?? TYPESET_DEFAULT.size,
|
|
351
|
+
leading: typeset?.leading ?? TYPESET_DEFAULT.leading,
|
|
352
|
+
flow: typeset?.flow ?? TYPESET_DEFAULT.flow,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The authored rhythm controls, as CSS declarations. Presets override exactly
|
|
358
|
+
* these — and *only* these.
|
|
359
|
+
*
|
|
360
|
+
* `partial` is the difference between the baseline and a preset. The baseline
|
|
361
|
+
* (`:root` / `.typeset`) fills every control from `TYPESET_DEFAULT` so the vars
|
|
362
|
+
* always resolve. A preset emits only what it authored, because it inherits the
|
|
363
|
+
* rest: a `.typeset-compact` that restated `--typeset-font-heading` would reset
|
|
364
|
+
* the folder's display face to the body font just by changing the size.
|
|
365
|
+
*/
|
|
366
|
+
export function typesetBaseVars(
|
|
367
|
+
typeset: Typeset | undefined,
|
|
368
|
+
indent = " ",
|
|
369
|
+
partial = false,
|
|
370
|
+
): string[] {
|
|
371
|
+
const t = partial ? (typeset ?? {}) : resolveTypeset(typeset);
|
|
372
|
+
const lines: string[] = [];
|
|
373
|
+
const push = (name: string, value: string) => lines.push(`${indent}--typeset-${name}: ${value};`);
|
|
374
|
+
if (t.size !== undefined) push("size", len(t.size));
|
|
375
|
+
if (t.leading !== undefined) push("leading", String(round(t.leading)));
|
|
376
|
+
if (t.flow !== undefined) push("flow", len(t.flow));
|
|
377
|
+
if (!partial || t.fontBody !== undefined) {
|
|
378
|
+
push("font-body", faceVar(t.fontBody, "inherit"));
|
|
379
|
+
}
|
|
380
|
+
if (!partial || t.fontHeading !== undefined) {
|
|
381
|
+
push("font-heading", faceVar(t.fontHeading, "var(--typeset-font-body)"));
|
|
382
|
+
}
|
|
383
|
+
if (!partial || t.fontMono !== undefined) {
|
|
384
|
+
push("font-mono", faceVar(t.fontMono, "var(--font-mono, ui-monospace, monospace)"));
|
|
385
|
+
}
|
|
386
|
+
return lines;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The derived scale, as CSS declarations referencing the authored controls.
|
|
391
|
+
*
|
|
392
|
+
* These MUST be emitted in the same rule as the authored controls. Custom
|
|
393
|
+
* property substitution resolves `var()` against the referencing element's own
|
|
394
|
+
* cascaded value, so co-locating them means a preset that overrides
|
|
395
|
+
* `--typeset-size` on the element re-derives the whole ladder for that subtree —
|
|
396
|
+
* and the `text-h1` utility and the `:where(.typeset h1)` rule then compute the
|
|
397
|
+
* *same* value, with no specificity fight to arbitrate.
|
|
398
|
+
*/
|
|
399
|
+
export function typesetVars(indent = " "): string[] {
|
|
400
|
+
const lines: string[] = [`${indent}--typeset-rhythm: var(--typeset-size);`];
|
|
401
|
+
for (const role of TYPESET_SCALE_NAMES) {
|
|
402
|
+
const ratio = TYPESET_RATIOS[role];
|
|
403
|
+
const size =
|
|
404
|
+
ratio.size === 1 ? "var(--typeset-rhythm)" : `calc(var(--typeset-rhythm) * ${ratio.size})`;
|
|
405
|
+
const leading =
|
|
406
|
+
ratio.leading === 1
|
|
407
|
+
? "var(--typeset-leading)"
|
|
408
|
+
: `calc(var(--typeset-leading) * ${ratio.leading})`;
|
|
409
|
+
lines.push(`${indent}--text-${role}: ${size};`);
|
|
410
|
+
lines.push(`${indent}--leading-${role}: ${leading};`);
|
|
411
|
+
lines.push(`${indent}--tracking-${role}: ${ratio.tracking};`);
|
|
412
|
+
}
|
|
413
|
+
return lines;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Width under which the base size gets a small bump, so phone-width frames read comfortably. */
|
|
417
|
+
const NARROW_BREAKPOINT = "48rem";
|
|
418
|
+
const NARROW_BUMP = 1.125;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The narrow-width bump rides on the *derived* `--typeset-rhythm` rather than on
|
|
422
|
+
* the authored `--typeset-size`, so it applies to every preset automatically
|
|
423
|
+
* without each one restating it.
|
|
424
|
+
*/
|
|
425
|
+
function rhythmBump(selector: string): string[] {
|
|
426
|
+
return [
|
|
427
|
+
`@media (width < ${NARROW_BREAKPOINT}) {`,
|
|
428
|
+
` ${selector} {`,
|
|
429
|
+
` --typeset-rhythm: calc(var(--typeset-size) * ${NARROW_BUMP});`,
|
|
430
|
+
" }",
|
|
431
|
+
"}",
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Element rules for a typeset region. Every selector is fully `:where()`-wrapped
|
|
437
|
+
* so the whole block has zero specificity and any authored utility or inline
|
|
438
|
+
* style wins. Spacing is `margin-block-start` only, with no `:last-child` /
|
|
439
|
+
* `:has()` / `:empty` — matching upstream's streaming guarantee that appending a
|
|
440
|
+
* block never restyles the ones before it.
|
|
441
|
+
*/
|
|
442
|
+
function typesetElementRules(): string[] {
|
|
443
|
+
const flow = "var(--typeset-flow)";
|
|
444
|
+
const rules: string[] = [
|
|
445
|
+
":where(.typeset) {",
|
|
446
|
+
" font-family: var(--typeset-font-body);",
|
|
447
|
+
" font-size: var(--typeset-rhythm);",
|
|
448
|
+
" line-height: var(--typeset-leading);",
|
|
449
|
+
"}",
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
for (const level of [1, 2, 3, 4, 5, 6]) {
|
|
453
|
+
const role = HEADING_ROLE_BY_LEVEL[level] as TypesetRole;
|
|
454
|
+
// Bigger headings earn more air above them; the multiplier rides on flow so
|
|
455
|
+
// the whole vertical rhythm still moves with one control.
|
|
456
|
+
const space = round(1.9 - (level - 1) * 0.15);
|
|
457
|
+
rules.push(
|
|
458
|
+
`:where(.typeset h${level}) {`,
|
|
459
|
+
" font-family: var(--typeset-font-heading);",
|
|
460
|
+
` font-size: var(--text-${role});`,
|
|
461
|
+
` line-height: var(--leading-${role});`,
|
|
462
|
+
` letter-spacing: var(--tracking-${role});`,
|
|
463
|
+
` font-weight: ${TYPESET_RATIOS[role].weight};`,
|
|
464
|
+
` margin-block-start: calc(${flow} * ${space});`,
|
|
465
|
+
" margin-block-end: 0;",
|
|
466
|
+
" text-wrap: balance;",
|
|
467
|
+
"}",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
rules.push(
|
|
472
|
+
// One selector per line: the flow list is long enough that a single line
|
|
473
|
+
// runs off the side of an editor, and this sheet is meant to be read.
|
|
474
|
+
":where(",
|
|
475
|
+
" .typeset p,",
|
|
476
|
+
" .typeset ul,",
|
|
477
|
+
" .typeset ol,",
|
|
478
|
+
" .typeset blockquote,",
|
|
479
|
+
" .typeset pre,",
|
|
480
|
+
" .typeset table,",
|
|
481
|
+
" .typeset figure",
|
|
482
|
+
") {",
|
|
483
|
+
` margin-block-start: ${flow};`,
|
|
484
|
+
" margin-block-end: 0;",
|
|
485
|
+
"}",
|
|
486
|
+
":where(.typeset > :first-child) {",
|
|
487
|
+
" margin-block-start: 0;",
|
|
488
|
+
"}",
|
|
489
|
+
":where(.typeset ul, .typeset ol) {",
|
|
490
|
+
` padding-inline-start: calc(${flow} * 1.25);`,
|
|
491
|
+
"}",
|
|
492
|
+
":where(.typeset ul) { list-style: disc; }",
|
|
493
|
+
":where(.typeset ol) { list-style: decimal; }",
|
|
494
|
+
":where(.typeset li) {",
|
|
495
|
+
` margin-block-start: calc(${flow} * 0.4);`,
|
|
496
|
+
"}",
|
|
497
|
+
":where(.typeset li)::marker {",
|
|
498
|
+
" color: var(--color-muted-foreground, currentColor);",
|
|
499
|
+
"}",
|
|
500
|
+
":where(.typeset blockquote) {",
|
|
501
|
+
` padding-inline-start: ${flow};`,
|
|
502
|
+
" border-inline-start: 2px solid var(--color-border, currentColor);",
|
|
503
|
+
" color: var(--color-muted-foreground, inherit);",
|
|
504
|
+
" font-style: italic;",
|
|
505
|
+
"}",
|
|
506
|
+
":where(.typeset code) {",
|
|
507
|
+
" font-family: var(--typeset-font-mono);",
|
|
508
|
+
" font-size: 0.875em;",
|
|
509
|
+
" border-radius: calc(var(--radius, 0.5rem) * 0.5);",
|
|
510
|
+
" background: var(--color-muted, transparent);",
|
|
511
|
+
" padding: 0.15em 0.35em;",
|
|
512
|
+
"}",
|
|
513
|
+
":where(.typeset pre) {",
|
|
514
|
+
" font-family: var(--typeset-font-mono);",
|
|
515
|
+
` padding: ${flow};`,
|
|
516
|
+
" border-radius: var(--radius, 0.5rem);",
|
|
517
|
+
" background: var(--color-muted, transparent);",
|
|
518
|
+
" overflow-x: auto;",
|
|
519
|
+
"}",
|
|
520
|
+
":where(.typeset pre code) {",
|
|
521
|
+
" background: none;",
|
|
522
|
+
" padding: 0;",
|
|
523
|
+
" font-size: 0.875em;",
|
|
524
|
+
"}",
|
|
525
|
+
":where(.typeset a) {",
|
|
526
|
+
" color: inherit;",
|
|
527
|
+
" text-decoration: underline;",
|
|
528
|
+
" text-underline-offset: 0.2em;",
|
|
529
|
+
"}",
|
|
530
|
+
":where(.typeset strong) { font-weight: 600; }",
|
|
531
|
+
":where(.typeset small) {",
|
|
532
|
+
" font-size: var(--text-small);",
|
|
533
|
+
" line-height: var(--leading-small);",
|
|
534
|
+
"}",
|
|
535
|
+
":where(.typeset hr) {",
|
|
536
|
+
` margin-block-start: calc(${flow} * 1.6);`,
|
|
537
|
+
" margin-block-end: 0;",
|
|
538
|
+
" border: 0;",
|
|
539
|
+
" border-block-start: 1px solid var(--color-border, currentColor);",
|
|
540
|
+
"}",
|
|
541
|
+
":where(.typeset table) {",
|
|
542
|
+
" width: 100%;",
|
|
543
|
+
" border-collapse: collapse;",
|
|
544
|
+
" font-size: var(--text-caption);",
|
|
545
|
+
" line-height: var(--leading-caption);",
|
|
546
|
+
"}",
|
|
547
|
+
":where(.typeset th, .typeset td) {",
|
|
548
|
+
` padding: calc(${flow} * 0.4) calc(${flow} * 0.6);`,
|
|
549
|
+
" border-block-end: 1px solid var(--color-border, currentColor);",
|
|
550
|
+
" text-align: start;",
|
|
551
|
+
"}",
|
|
552
|
+
":where(.typeset th) { font-weight: 600; }",
|
|
553
|
+
":where(.typeset img, .typeset video) {",
|
|
554
|
+
" max-width: 100%;",
|
|
555
|
+
" height: auto;",
|
|
556
|
+
" border-radius: var(--radius, 0.5rem);",
|
|
557
|
+
"}",
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
return rules;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export interface TypesetCssOptions {
|
|
564
|
+
/**
|
|
565
|
+
* Emit the `:root` baseline (the folder default projected globally, so
|
|
566
|
+
* `text-h1` works outside a typeset region). The `.typeset` region rules are
|
|
567
|
+
* always emitted. Defaults to true.
|
|
568
|
+
*/
|
|
569
|
+
root?: boolean;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* The complete typeset stylesheet for a folder's typesets. One generator, used
|
|
574
|
+
* verbatim by the renderer (canvas + screenshots) and by `emit_theme` (the
|
|
575
|
+
* `typeset.css` artifact) so the design and the generated app cannot diverge.
|
|
576
|
+
*/
|
|
577
|
+
export function typesetCss(
|
|
578
|
+
typesets: Record<string, Typeset> | undefined,
|
|
579
|
+
options: TypesetCssOptions = {},
|
|
580
|
+
): string {
|
|
581
|
+
const all = typesets ?? {};
|
|
582
|
+
const base = all[DEFAULT_TYPESET_NAME];
|
|
583
|
+
const blocks: string[] = [];
|
|
584
|
+
|
|
585
|
+
if (options.root !== false) {
|
|
586
|
+
blocks.push([":root {", ...typesetBaseVars(base), ...typesetVars(), "}"].join("\n"));
|
|
587
|
+
blocks.push(rhythmBump(":root").join("\n"));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// The region baseline. Authored controls and the derived scale share this rule
|
|
591
|
+
// so a preset class on the same element re-derives the ladder.
|
|
592
|
+
blocks.push([".typeset {", ...typesetBaseVars(base), ...typesetVars(), "}"].join("\n"));
|
|
593
|
+
blocks.push(rhythmBump(".typeset").join("\n"));
|
|
594
|
+
|
|
595
|
+
for (const [name, typeset] of Object.entries(all)) {
|
|
596
|
+
if (name === DEFAULT_TYPESET_NAME) continue;
|
|
597
|
+
if (!isTypesetName(name)) continue;
|
|
598
|
+
// Presets override the authored controls only — the derived scale declared
|
|
599
|
+
// on `.typeset` re-substitutes against these on the same element. And only
|
|
600
|
+
// the controls the preset actually authored, so tuning one does not silently
|
|
601
|
+
// reset the others to the baseline.
|
|
602
|
+
const declarations = typesetBaseVars(typeset, " ", true);
|
|
603
|
+
if (declarations.length === 0) continue;
|
|
604
|
+
blocks.push([`.typeset-${name} {`, ...declarations, "}"].join("\n"));
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
blocks.push(typesetElementRules().join("\n"));
|
|
608
|
+
|
|
609
|
+
return blocks.join("\n\n");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** A role's proportions resolved to concrete values, for native framework themes. */
|
|
613
|
+
export interface ResolvedTypesetRole {
|
|
614
|
+
/** Font size in px. */
|
|
615
|
+
fontSize: number;
|
|
616
|
+
/** Unitless line-height. */
|
|
617
|
+
lineHeight: number;
|
|
618
|
+
/** Letter-spacing, as an `em` string. */
|
|
619
|
+
letterSpacing: string;
|
|
620
|
+
fontWeight: number;
|
|
621
|
+
/** The resolved font stack for this role's face, when the typeset names one. */
|
|
622
|
+
fontFamily?: string;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export type TypesetScale = Record<TypesetRole, ResolvedTypesetRole>;
|
|
626
|
+
|
|
627
|
+
export interface TypesetScaleOptions {
|
|
628
|
+
/**
|
|
629
|
+
* Pixel value for a relative base size. The default `1em` is
|
|
630
|
+
* container-relative, which a serialized framework theme cannot express, so
|
|
631
|
+
* native adapters resolve it against the document root.
|
|
632
|
+
*/
|
|
633
|
+
rootPx?: number;
|
|
634
|
+
/** `typography.fontFamily`, for resolving `fontBody` / `fontHeading` roles to real stacks. */
|
|
635
|
+
fontFamily?: Record<string, string>;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const DEFAULT_ROOT_PX = 16;
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Resolve an authored length to px. Relative units scale `rootPx` by their
|
|
642
|
+
* factor. Exported because every surface that turns an authored control back
|
|
643
|
+
* into a number — the native scale below, the canvas's rhythm sliders — has to
|
|
644
|
+
* agree on what `"1em"` and a bare `15` mean.
|
|
645
|
+
*/
|
|
646
|
+
export function typesetSizePx(size: string | number, rootPx = DEFAULT_ROOT_PX): number {
|
|
647
|
+
if (typeof size === "number") return size;
|
|
648
|
+
const match = /^(-?[\d.]+)\s*(px|rem|em|%)?$/.exec(size.trim());
|
|
649
|
+
if (!match) return rootPx;
|
|
650
|
+
const n = Number.parseFloat(match[1] as string);
|
|
651
|
+
if (!Number.isFinite(n)) return rootPx;
|
|
652
|
+
switch (match[2]) {
|
|
653
|
+
case "px":
|
|
654
|
+
case undefined:
|
|
655
|
+
return n;
|
|
656
|
+
case "%":
|
|
657
|
+
return (n / 100) * rootPx;
|
|
658
|
+
default:
|
|
659
|
+
return n * rootPx;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* The same ratios as `typesetVars`, resolved to concrete values. Native adapters
|
|
665
|
+
* (MUI/antd/chakra) project this onto their own typography scales, and the very
|
|
666
|
+
* same call feeds both the canvas mount and the emitted theme artifact — so a
|
|
667
|
+
* preview and generated code cannot disagree.
|
|
668
|
+
*/
|
|
669
|
+
export function typesetScale(
|
|
670
|
+
typeset: Typeset | undefined,
|
|
671
|
+
options: TypesetScaleOptions = {},
|
|
672
|
+
): TypesetScale {
|
|
673
|
+
const t = resolveTypeset(typeset);
|
|
674
|
+
const rootPx = options.rootPx ?? DEFAULT_ROOT_PX;
|
|
675
|
+
const basePx = typesetSizePx(t.size, rootPx);
|
|
676
|
+
const families = options.fontFamily ?? {};
|
|
677
|
+
const headingStack = t.fontHeading ? families[t.fontHeading] : undefined;
|
|
678
|
+
const bodyStack = t.fontBody ? families[t.fontBody] : undefined;
|
|
679
|
+
|
|
680
|
+
const out = {} as TypesetScale;
|
|
681
|
+
for (const role of TYPESET_SCALE_NAMES) {
|
|
682
|
+
const ratio = TYPESET_RATIOS[role];
|
|
683
|
+
const stack = ratio.face === "heading" ? (headingStack ?? bodyStack) : bodyStack;
|
|
684
|
+
out[role] = {
|
|
685
|
+
fontSize: round(basePx * ratio.size),
|
|
686
|
+
lineHeight: round(t.leading * ratio.leading),
|
|
687
|
+
letterSpacing: ratio.tracking,
|
|
688
|
+
fontWeight: ratio.weight,
|
|
689
|
+
...(stack ? { fontFamily: stack } : {}),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return out;
|
|
693
|
+
}
|