diff --git a/frontend/purestack.studio/guides/content/_nav.json b/frontend/purestack.studio/guides/content/_nav.json index 93c58dab..1ba18d86 100644 --- a/frontend/purestack.studio/guides/content/_nav.json +++ b/frontend/purestack.studio/guides/content/_nav.json @@ -3,6 +3,7 @@ "index.mdx", "links.mdx", "code-blocks.mdx", + "image-optimization.mdx", "shared-content.mdx", "localization.mdx" ] diff --git a/frontend/purestack.studio/guides/content/image-optimization.mdx b/frontend/purestack.studio/guides/content/image-optimization.mdx new file mode 100644 index 00000000..87722492 --- /dev/null +++ b/frontend/purestack.studio/guides/content/image-optimization.mdx @@ -0,0 +1,286 @@ +--- +title: Image optimization +description: Enable responsive images with ts-images, choose widths and formats, prioritize hero images, and reuse optimized assets across builds. +template: doc +layout: + showNav: true + showToc: true + showFooter: true +nav: + icon: tabler:photo +--- + +# Image optimization + +`@purestack/ts-images` turns local images into responsive AVIF and WebP sources with JPEG or PNG fallbacks during the build. Keep writing ordinary Markdown or HTML images. The plugin adds responsive markup, missing dimensions, and loading defaults without a special component or browser script. + +Image optimization is optional and is not included in PureStack's default plugin set. Original files remain available for downloads and links, and source files are never edited. + +## Enable the plugin + +Install the package alongside PureStack. It requires Node.js 22.20 or newer and uses Sharp's platform-specific image codecs: + +```sh +npm install @purestack/ts-images +``` + +Add the named plugin export to `content/siteConfig.json`, keeping any plugins already listed: + +```json +{ + "plugins": ["@purestack/ts-images#imagesPlugin"], + "extensions": { + "images": { + "widths": [480, 960, 1440], + "formats": ["avif", "webp"] + } + } +} +``` + +No TypeScript configuration is required. Listing the plugin enables its defaults; the `extensions.images` section is optional. Settings in that section belong to the image plugin and are applied only when it is installed in the plugin list. + +Place images inside the content folder and reference them from your pages: + +```text +content/ + siteConfig.json + index.mdx + images/ + dashboard.png +``` + +```md +![Dashboard showing project activity](./images/dashboard.png) +``` + +Run your usual [build or publish command](/guides/getting-started/purestack-cli/). The same optimization applies to images in rendered components, page templates, and shared headers and footers. Images inserted later by browser JavaScript are outside the build's rendered document. + +## What visitors receive + +For an eligible image, the plugin generates a `` with AVIF and WebP sources and an `` fallback. Each format has a `srcset` of available widths. The browser chooses a supported format and a suitable candidate for the image's layout and the screen's pixel density. + +The plugin preserves `alt`, classes, IDs, styles, links around images, and author-supplied loading attributes. It fills missing `width` and `height` attributes from the image's oriented aspect ratio so the browser can reserve space. If you supply one dimension, the other is derived; if you supply both, both are kept. + +Generated images are written under `/_assets/images/`, with your site's `basePath` applied automatically. Filenames include a hash of the encoded bytes. Shared variants are reused across pages, and generated files are removed when no page references them. Original assets remain available at their usual URLs. + +Widths are sorted, deduplicated, and capped at the source image's dimensions. A 700-pixel-wide source with widths `[480, 960, 1440]` produces 480- and 700-pixel candidates for each format. The plugin never enlarges a source image. + +## Match responsive images to your layout + +`widths` chooses the pixel widths to generate. `sizes` tells the browser how wide the image will appear in the layout. It does not set the image's CSS width. + +The default `sizes` is `100vw`, suitable for an image that spans the viewport. For an image that spans the viewport only up to a 960-pixel limit, configure: + +```json +{ + "extensions": { + "images": { + "widths": [480, 960, 1440, 1920], + "sizes": "(max-width: 960px) 100vw, 960px" + } + } +} +``` + +Adjust `sizes` for your container widths, sidebars, and gutters. A browser may choose a larger candidate for a high-density screen, so a 960-pixel layout can benefit from a 1920-pixel source. + +An individual image can supply its own `sizes`, overriding the configured default: + +```html +Dashboard showing project activity +``` + +Use HTML when you need per-image attributes. Markdown images use the global settings. More widths and formats generate more files and take more encoding time; choose candidates that serve your actual layouts. + +## Use presets for common images + +Presets group per-image choices. Select one with `data-image-preset`: + +```html +The workshop +Editor settings +A contributor +``` + +| Preset | Behavior | +| --- | --- | +| `content` | Uses the global image settings. | +| `hero` | Generates 960, 1440, and 1920 widths and supplies eager loading with high fetch priority. | +| `thumbnail` | Generates 160, 320, and 640 widths with `sizes="(max-width: 640px) 50vw, 320px"`. | +| `screenshot` | Uses lossless WebP with a PNG fallback and inherits the global widths. | + +Preset widths are still capped at the source's dimensions. Presets inherit the resolved global settings before applying their own overrides. Override a built-in preset or add your own under `presets`: + +```json +{ + "extensions": { + "images": { + "presets": { + "card": { + "widths": [320, 640, 960], + "sizes": "(max-width: 640px) 100vw, 480px", + "quality": { "webp": 85 } + } + } + } + } +} +``` + +```html +Project overview +``` + +Presets can set `widths`, `formats`, `quality`, `sizes`, `loading`, `decoding`, `priority`, `lossless`, and `metadata`. + +### Prioritize the main image + +Images default to `loading="lazy"` and `decoding="async"`. For a prominent image visible when the page opens, use the `hero` preset or mark it directly: + +```html +The project workspace +``` + +Priority fills missing attributes with `loading="eager"` and `fetchpriority="high"`. Explicit `loading`, `decoding`, and `fetchpriority` attributes take precedence, so remove `loading="lazy"` if you want the priority default to apply. Mark the important image explicitly; the plugin does not infer which images are above the fold. + +## Choose formats, quality, and metadata + +The default formats are AVIF followed by WebP, with an automatic JPEG or PNG fallback. The order in `formats` determines source preference. PNG sources, transparent images, and lossless presets use a PNG fallback. + +```json +{ + "extensions": { + "images": { + "formats": ["avif", "webp"], + "quality": { "avif": 50, "webp": 80, "jpeg": 82 }, + "metadata": "strip" + } + } +} +``` + +Quality values range from 1 to 100. Use `lossless: true` when preserving detail matters more than file size, as with screenshots and diagrams. The `screenshot` preset provides that choice per image. + +Set `formats: []` to generate only responsive JPEG/PNG fallbacks without adding a `` wrapper. This can help when existing CSS depends on the image being a direct child of its parent. + +Images are automatically oriented and converted to sRGB. `metadata: "strip"` omits embedded metadata by default. `"preserve"` retains metadata with orientation normalized. Optimization can produce files larger than the original, especially for small or lossless images; inspect the results for representative sources before choosing settings for the whole site. + +## Control which images are processed + +Use `include` and `exclude` to select source files. Patterns match content-root-relative paths with forward slashes, regardless of the page referencing the image: + +```json +{ + "extensions": { + "images": { + "include": ["images/**"], + "exclude": ["images/downloads/**"] + } + } +} +``` + +To keep one image's markup and source unchanged, opt out on that image: + +```html +Source diagram +``` + +To turn off all optimization while keeping the plugin declaration, set `extensions.images.enabled` to `false`. You can also remove the plugin from `plugins`. + +| Input | Treatment | +| --- | --- | +| Local JPEG, PNG, WebP, AVIF, single-frame GIF, and TIFF references | Eligible for optimization. | +| External, data, and blob URLs | Left unchanged; the plugin does not download images. | +| SVG and animated images, including APNG | Left unchanged. | +| An image inside ``, or one with `srcset` already set | Existing responsive markup is preserved. | +| CSS backgrounds, video posters, social preview metadata, and URLs in browser scripts | Original assets remain available; these references are not rewritten. | + +Because optimized images normally gain a `` parent, review direct-child selectors such as `.gallery > img`. Adjust the selector, use fallback-only output, or opt out where the original structure is required. + +## Builds, development, and caching + +Full builds and publishes process images referenced by rendered pages. During `serve`, images are processed when their pages render, and generated files exist before those pages are served. Editing a source image invalidates dependent pages. Local paths follow the same [content link and localization rules](./links) as other assets. + +The persistent encoding cache survives cleaned output directories. Its keys include source bytes, pixel settings, and codec versions. Changes to `alt`, loading attributes, or `sizes` can reuse encoded files. The default cache lives in the OS temporary directory under `purestack-images`, separated by content root. + +`cacheMaxAgeDays` defaults to 30. During use, cleanup removes entries unused for that many days, checking at most hourly per processor. Set it to `0` to disable cleanup. A custom `cacheDir` must stay outside the content and output directories; relative paths resolve from the build's `rootDir`, not the content folder. The default temporary cache needs no path configuration. + +To bypass persistent cache reads and writes for a run: + +```sh +yarn purestack publish --content ./content --no-cache +``` + +The same flag works with `build` and `serve`. Programmatic builds use `options.cache: false`. Existing cache files remain in place, and development still reuses rendered pages until they are invalidated. See the [CLI guide](/guides/getting-started/purestack-cli/) for cache and output-cleaning behavior. + +Encoding concurrency defaults to two jobs per site, including output emission. Increase it only after measuring duration and memory on your build machine. Keeping fewer widths or formats also reduces work on a cache miss. + +## Configure from TypeScript + +For custom factory defaults or diagnostic callbacks, add the plugin to your existing `purestack.config.ts`: + +```ts +import { imagesPlugin } from '@purestack/ts-images' +import { defineConfig } from 'purestack' + +export default defineConfig({ + plugins: [ + imagesPlugin({ + widths: [480, 960, 1440], + onDiagnostic(diagnostic) { + console.log(diagnostic) + }, + }), + ], +}) +``` + +Factory settings supply defaults that `extensions.images` can override. An explicit TypeScript plugin instance replaces a JSON-selected instance with the same plugin name. Callbacks belong in TypeScript; JSON supplies settings only. The [Plugins guide](/guides/extending/plugins/) explains how plugin declarations compose. + +Diagnostics report the referring page, source path, cache result, original bytes, total generated bytes, variant count, and duration. Without a callback, completed optimizations are logged as `image optimized`. Total generated bytes include every variant; they are not the bytes a visitor downloads for a single image. + +## Settings reference + +All settings below belong under `extensions.images`, or can be supplied as factory defaults to `imagesPlugin()`. + +| Setting | Default | Purpose | +| --- | --- | --- | +| `enabled` | `true` | Enable optimization when the plugin is listed. | +| `widths` | `[480, 960, 1440]` | 1 to 16 widths, each an integer from 1 to 16384; no upscaling. | +| `formats` | `["avif", "webp"]` | Modern sources in preference order; `[]` generates only fallbacks. | +| `quality` | `{ "avif": 50, "webp": 80, "jpeg": 82 }` | Encoder quality from 1 to 100. | +| `sizes` | `"100vw"` | Default layout width hint; an image's own `sizes` takes precedence. | +| `loading` | `"lazy"` | `"lazy"`, `"eager"`, or `false` to omit the default attribute. | +| `decoding` | `"async"` | `"async"`, `"sync"`, `"auto"`, or `false`. | +| `priority` | `false` | Supply missing eager-loading and high-priority attributes. | +| `lossless` | `false` | Use lossless modern encoders and a PNG fallback. | +| `metadata` | `"strip"` | Strip metadata or use `"preserve"`. | +| `include` / `exclude` | `["**/*"]` / `[]` | Source selection using content-root-relative glob patterns. | +| `presets` | `content`, `hero`, `thumbnail`, `screenshot` | Override built-ins or define named per-image settings. | +| `cacheDir` | OS temporary cache per content root | Location outside content and output directories. | +| `cacheMaxAgeDays` | `30` | Retain entries used within this many days; `0` disables expiry. | +| `concurrency` | `2` | Per-site processing limit from 1 to 16. | +| `maxInputPixels` | `40000000` | Maximum source pixel count. | +| `maxSourceBytes` | `52428800` (50 MiB) | Maximum source file size. | +| `onError` | `"error"` | Fail on optimization errors, or use `"warn"` to retain the original image. | + +## When something goes wrong + +| Symptom | Check | +| --- | --- | +| Images still reference their originals | Confirm the plugin is listed, `enabled` is true, the file matches `include`/`exclude`, and the input is eligible. Existing `` and `srcset` are intentionally preserved. | +| A hero still loads lazily | Remove an explicit `loading="lazy"`; author-supplied attributes override priority defaults. | +| The browser selects an unexpectedly large candidate | Match `sizes` to the actual layout and account for screen pixel density. | +| A gallery's CSS stops matching | Optimization added a `` parent. Adjust direct-child selectors or opt out. | +| The build reports an unknown preset | Define the name under `presets` or choose one of the built-in presets. | +| A source exceeds the byte or pixel limit | Resize the source, exclude it, or deliberately adjust the corresponding limit. | +| Sharp cannot load on the build machine | Install dependencies for that machine's platform and confirm Node.js 22.20 or newer. | + +`onError: "warn"` lets the plugin keep an original image after an optimization failure, but missing or invalid local URLs still follow PureStack's normal URL validation. Remote downloads, SVG optimization, animated transcoding, placeholders, and automatic cropping are outside this plugin's current scope. diff --git a/frontend/purestack.studio/guides/content/index.mdx b/frontend/purestack.studio/guides/content/index.mdx index 5b8e0194..adcb0604 100644 --- a/frontend/purestack.studio/guides/content/index.mdx +++ b/frontend/purestack.studio/guides/content/index.mdx @@ -1,6 +1,6 @@ --- title: Content -description: Write links and code examples, share content, and organize translations. +description: Write links and code examples, optimize images, share content, and organize translations. template: doc nav: icon: tabler:files @@ -10,9 +10,10 @@ layout: # Content -Write links and code examples, share content, and organize translations. +Write links and code examples, optimize images, share content, and organize translations. - [Links](./links) - [Code blocks](./code-blocks) +- [Image optimization](./image-optimization) - [Shared content](./shared-content) - [Localization](./localization) diff --git a/frontend/purestack.studio/guides/content/links.mdx b/frontend/purestack.studio/guides/content/links.mdx index 52156a43..e2ea626c 100644 --- a/frontend/purestack.studio/guides/content/links.mdx +++ b/frontend/purestack.studio/guides/content/links.mdx @@ -89,6 +89,8 @@ Images, videos, downloads, and other files in your content folder follow the sam Write a file's full name, with its extension and the same capitals, so it works on any host. +For responsive image formats, sizing, and loading defaults, see [Image optimization](./image-optimization). The optional `ts-images` plugin handles ordinary Markdown and rendered HTML images while keeping originals available for downloads. + Files that a build step writes straight into the output aren't in your content folder, so PureStack can't check them. Link them from the site root, such as `/previews/demo.html`. ## Link outside your content diff --git a/frontend/purestack.studio/guides/extending/plugins.mdx b/frontend/purestack.studio/guides/extending/plugins.mdx index 4d98b04f..e2926513 100644 --- a/frontend/purestack.studio/guides/extending/plugins.mdx +++ b/frontend/purestack.studio/guides/extending/plugins.mdx @@ -64,6 +64,8 @@ its defaults even without `extensions.images`. Set `extensions.images.enabled` to `false` to disable optimization. An explicit plugin instance in `purestack.config.ts` replaces a JSON-selected plugin with the same name. +The [Image optimization guide](/guides/content/image-optimization/) covers responsive sizing, presets, formats, caching, and per-image controls. + Both config formats use PureStack's own copy of a package when available so shared registries stay together; other packages resolve from the site. Plugin declarations execute installed JavaScript with the build process's @@ -242,18 +244,7 @@ Builder style state is isolated across asynchronous operations, including concur ## Optimize images -Image optimization is optional. Install `@purestack/ts-images` alongside PureStack and add it to your existing plugin list: - -```ts -import { defineConfig } from 'purestack' -import { imagesPlugin } from '@purestack/ts-images' - -export default defineConfig({ - plugins: [imagesPlugin({ widths: [480, 960, 1440] })], -}) -``` - -Ordinary Markdown images and rendered HTML images receive responsive AVIF/WebP sources, JPEG/PNG fallbacks, dimensions, and loading defaults. Originals remain available for links and downloads. Remote images, SVGs, animations, existing `picture` markup, and images with `srcset` pass through. Use `data-image-preset="hero"` for eager priority images, `data-image-preset="screenshot"` for lossless output, or `data-image-optimize="false"` to keep an individual image unchanged. Set `sizes` for your actual image layout. +For installation and site configuration, follow the dedicated [Image optimization guide](/guides/content/image-optimization/). It covers JSON and TypeScript setup, responsive sizing, presets, caching, and per-image controls. The rest of this section explains the asset contribution for plugin authors. The plugin owns its `extensions.images` settings, persistent cache, and encoder limits. It uses the generic asset contribution: `assets(config)` returns a per-site processor with a unique namespace and `processPage(input)`. This runs after all `onPageDocument` hooks, before URL rewriting. `resolveSource(element, url)` resolves local inputs and registers dependencies; `dependOn(relPath)` registers additional inputs; `emit(fileName, bytes)` atomically writes an owned file and returns its URL. Use content-addressed names and await all writes. Core handles base paths, incremental invalidation, shared references, manifest persistence, and cleanup when pages or processors disappear. The builder contains no image format or encoder selection. diff --git a/frontend/purestack.studio/guides/index.mdx b/frontend/purestack.studio/guides/index.mdx index 0171e23d..9b9854d4 100644 --- a/frontend/purestack.studio/guides/index.mdx +++ b/frontend/purestack.studio/guides/index.mdx @@ -23,7 +23,7 @@ Start with the CLI and site configuration, then choose a topic below. Each categ

Content

-

Write links and code examples, share content, and organize translations.

+

Write links and code examples, optimize images, share content, and organize translations.

Explore content
diff --git a/package.json b/package.json index 3c7c783a..fe176330 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "purestack-repo", - "version": "1.1.16", + "version": "1.1.17", "packageManager": "yarn@4.18.1", "type": "module", "sideEffects": false, diff --git a/packages/purestack/package.json b/packages/purestack/package.json index 50f09052..8fdb6139 100644 --- a/packages/purestack/package.json +++ b/packages/purestack/package.json @@ -1,6 +1,6 @@ { "name": "purestack", - "version": "1.1.16", + "version": "1.1.17", "packageManager": "yarn@4.9.2", "module": "./dist/purestack.mjs", "types": "./dist/purestack.d.mts", diff --git a/packages/purestack/src/index.ts b/packages/purestack/src/index.ts index 2c4f7b73..28b0b535 100644 --- a/packages/purestack/src/index.ts +++ b/packages/purestack/src/index.ts @@ -1,3 +1,3 @@ -export const version: string = '1.1.16' +export const version: string = '1.1.17' export * from '@purestack/ts-ssg' diff --git a/packages/ts-common/package.json b/packages/ts-common/package.json index d447ea5f..58e7722a 100644 --- a/packages/ts-common/package.json +++ b/packages/ts-common/package.json @@ -1,6 +1,6 @@ { "name": "@purestack/ts-common", - "version": "1.1.16", + "version": "1.1.17", "packageManager": "yarn@4.9.2", "module": "./dist/ts-common.mjs", "types": "./dist/ts-common.d.mts", diff --git a/packages/ts-components/package.json b/packages/ts-components/package.json index 70723797..f914ac69 100644 --- a/packages/ts-components/package.json +++ b/packages/ts-components/package.json @@ -1,6 +1,6 @@ { "name": "@purestack/ts-components", - "version": "1.1.16", + "version": "1.1.17", "packageManager": "yarn@4.9.2", "module": "./dist/ts-components.mjs", "types": "./dist/ts-components.d.mts", diff --git a/packages/ts-css/README.md b/packages/ts-css/README.md index b24957a2..2ee10292 100644 --- a/packages/ts-css/README.md +++ b/packages/ts-css/README.md @@ -94,6 +94,35 @@ style.css({ display: 'grid', gap: '1rem' }) style.media('max-width: 720px').css({ display: 'block' }) ``` +### Keyframe Animations + +Use `.keyframes(name)` to create an animation definition and `.select()` to +define its frames. Frame selectors are emitted without the parent selector. + +```ts +const style = s() +const frames = style.keyframes('zt-memory-cycle') +frames.select('0%, 4%, 40%, 100%').transform('translateY(0)') +frames.select('12%, 28%').transform('translateY(-8px)') + +style.select('.zt-slab-memory') + .animation('zt-memory-cycle 8s ease-in-out infinite') +style.select('.zt-slab-memory') + .media('prefers-reduced-motion: reduce') + .animation('none !important') +``` + +Frames support `from`, `to`, percentages, and comma-separated percentage lists. +Use existing property methods, `.css()`, or `.set()` for declarations, including +motion-path properties such as `.set('offset-distance', '100%')`. +Repeated names in the same container reuse their block; repeated frame selectors +merge declarations using the normal style builder behavior. Definitions stay +inside their containing media or scope block and participate in revision tracking. + +Names are trimmed and must be non-empty; supply a valid CSS animation name and +valid frame selectors. Names are emitted without rewriting and remain global +inside `@scope`. Use distinct names for animations with different definitions. + ### CSS Variables via `autoVar` Values beginning with `$name` or `--name` are automatically converted to `var(...)`. @@ -189,7 +218,8 @@ const button = s('.button').css({ - `Style` extends `BaseStyle` and stores declarations in a map and nested selectors as children. - `.select()` creates a child style using the parent selector + the new selector (with automatic spacing unless a combinator or `:` is used). - `.media()` creates a sibling style under `@media(...)` and targets the current selector inside that media block. -- `.toCSS()` serializes rules in a deterministic order: normal selectors first, then `@media` blocks, preserving creation order via internal IDs. +- `.keyframes()` creates a named animation block whose children are unprefixed frame selectors. +- `.toCSS()` serializes each container in a deterministic order: normal selectors first, then `@media`, `@scope`, and `@keyframes` blocks, preserving creation order within each group via internal IDs. - `.toPrettyCSS()` runs the output through Prettier (`parser: css`) with default formatting options. ## API Reference @@ -204,6 +234,7 @@ Creates a `Style` instance. If a selector is provided, it becomes the root selec - `css(object)` - set many properties at once using `CSSProps`. - `select(selector)` - create or access a nested selector. - `media(query)` - create or access a `@media(...)` block scoped to this selector. +- `keyframes(name)` - create or access a named animation; select its frames with `select()`. - `use(style)` - shallow-merge declarations from another style into this one. - `toCSS()` - serialize to CSS string. - `toPrettyCSS(options?)` - serialize and format using Prettier. @@ -266,6 +297,7 @@ Key `BaseStyle` methods: - `css({...})` - `select(selector)` - `media(query)` +- `keyframes(name)` - `use(style)` - `toCSS()` - `toPrettyCSS(options?)` @@ -285,6 +317,22 @@ const stops = getGradient('#21a8c3', '#a50d60', 6, 'hsl') `CSSProps` is a large type union of CSS properties used by `.css()` and property-specific methods on `Style`. It provides autocomplete and helps catch invalid property names or values. +## Maintaining Property Metadata + +The generator combines the original `src/meta/cssProps.ts` with the handwritten +additions in `src/meta/cssProps2.ts`. Maintain new properties in `cssProps2.ts` +using the same `$`, `desc`, and optional `values.value` structure. Existing +properties, including vendor-prefixed and older names, remain available. + +Regenerate the fluent methods and explicit property types from the repository root: + +```bash +yarn tsx packages/ts-css/src/meta/generateStyle.ts +``` + +This updates `src/style.ts` and `src/cssProps.ts`. Keep property additions in the +metadata so they survive subsequent generator runs. + ## Tests ```bash diff --git a/packages/ts-css/package.json b/packages/ts-css/package.json index 67fa2152..d8cf0ec6 100644 --- a/packages/ts-css/package.json +++ b/packages/ts-css/package.json @@ -1,6 +1,6 @@ { "name": "@purestack/ts-css", - "version": "1.1.16", + "version": "1.1.17", "packageManager": "yarn@4.9.2", "module": "./dist/ts-css.mjs", "types": "./dist/ts-css.d.mts", diff --git a/packages/ts-css/src/baseStyle.ts b/packages/ts-css/src/baseStyle.ts index 0dcb0814..29f992c3 100644 --- a/packages/ts-css/src/baseStyle.ts +++ b/packages/ts-css/src/baseStyle.ts @@ -136,6 +136,17 @@ export class BaseStyle extends RootStyle { return child } + /** + * Create or access a named animation block. Select frames with `select()`: + * `style.keyframes('fade-in').select('from').opacity('0')`. + * Names are emitted as supplied and remain global inside `@scope`. + */ + keyframes(name: string) { + const trimmed = name.trim() + if (!trimmed) throw new Error('Keyframes name must be a non-empty string.') + return this.selectWithoutParentKey(`@keyframes ${trimmed}`) + } + toCSS() { let result = '' const children = [...this.children.entries()].sort((a, b) => { @@ -147,11 +158,11 @@ export class BaseStyle extends RootStyle { }) const selector = this.selector if (isGroupingRule(selector)) { - const mediaContent = children + const blockContent = children .map((x) => `\r\n${x[1].toCSS()}`) .join('') .trimEnd() - result = `${selector} {${mediaContent} + result = `${selector} {${blockContent} } ` @@ -210,7 +221,11 @@ ${props} } function isGroupingRule(selector: string) { - return selector.startsWith('@media') || selector.startsWith('@scope') + return ( + selector.startsWith('@media') || + selector.startsWith('@scope') || + selector.startsWith('@keyframes') + ) } function buildScopeRule(rootSelector: string, limitSelector?: string) { diff --git a/packages/ts-css/src/cssProps.ts b/packages/ts-css/src/cssProps.ts index b8f75029..af055290 100644 --- a/packages/ts-css/src/cssProps.ts +++ b/packages/ts-css/src/cssProps.ts @@ -1905,4 +1905,194 @@ export type CSSProps = { | (string & {}) zIndex: number | string zoom: 'normal' | (string & {}) + accentColor: 'auto' | (string & {}) + animationComposition: 'replace' | 'add' | 'accumulate' | (string & {}) + appearance: 'none' | 'auto' | (string & {}) + aspectRatio: 'auto' | (string & {}) + backgroundPositionX: 'left' | 'center' | 'right' | (string & {}) + backgroundPositionY: 'top' | 'center' | 'bottom' | (string & {}) + borderBlock: string + borderBlockColor: string + borderBlockStyle: string + borderBlockWidth: string + borderEndEndRadius: string + borderEndStartRadius: string + borderInline: string + borderInlineColor: string + borderInlineStyle: string + borderInlineWidth: string + borderStartEndRadius: string + borderStartStartRadius: string + breakAfter: 'auto' | 'avoid' | 'page' | 'column' | (string & {}) + breakBefore: 'auto' | 'avoid' | 'page' | 'column' | (string & {}) + breakInside: 'auto' | 'avoid' | 'avoid-page' | 'avoid-column' | (string & {}) + colorScheme: 'normal' | 'light' | 'dark' | (string & {}) + columnRuleColor: string + containIntrinsicBlockSize: string + containIntrinsicHeight: string + containIntrinsicInlineSize: string + containIntrinsicSize: string + containIntrinsicWidth: string + container: string + containerName: string + containerType: + | 'normal' + | 'size' + | 'inline-size' + | 'scroll-state' + | (string & {}) + contentVisibility: 'visible' | 'auto' | 'hidden' | (string & {}) + counterSet: string + cx: string + cy: string + dominantBaseline: + | 'auto' + | 'alphabetic' + | 'middle' + | 'central' + | 'hanging' + | (string & {}) + fontOpticalSizing: 'auto' | 'none' | (string & {}) + fontPalette: 'normal' | 'light' | 'dark' | (string & {}) + fontSynthesisSmallCaps: 'auto' | 'none' | (string & {}) + fontSynthesisStyle: 'auto' | 'none' | (string & {}) + fontSynthesisWeight: 'auto' | 'none' | (string & {}) + fontVariationSettings: string + hyphenateCharacter: string + inset: string + insetBlock: string + insetBlockEnd: string + insetBlockStart: string + insetInline: string + insetInlineEnd: string + insetInlineStart: string + lineClamp: 'none' | (string & {}) + marginBlock: string + marginInline: string + mask: string + maskClip: + | 'border-box' + | 'padding-box' + | 'content-box' + | 'no-clip' + | (string & {}) + maskComposite: 'add' | 'subtract' | 'intersect' | 'exclude' | (string & {}) + mathStyle: 'normal' | 'compact' | (string & {}) + offset: string + offsetAnchor: string + offsetDistance: string + offsetPath: string + offsetPosition: string + offsetRotate: 'auto' | 'reverse' | (string & {}) + overflowBlock: + | 'visible' + | 'hidden' + | 'clip' + | 'scroll' + | 'auto' + | (string & {}) + overflowInline: + | 'visible' + | 'hidden' + | 'clip' + | 'scroll' + | 'auto' + | (string & {}) + overscrollBehavior: string + overscrollBehaviorBlock: 'auto' | 'contain' | 'none' | (string & {}) + overscrollBehaviorInline: 'auto' | 'contain' | 'none' | (string & {}) + overscrollBehaviorX: 'auto' | 'contain' | 'none' | (string & {}) + overscrollBehaviorY: 'auto' | 'contain' | 'none' | (string & {}) + paddingBlock: string + paddingInline: string + page: string + placeContent: string + placeItems: string + placeSelf: string + printColorAdjust: 'economy' | 'exact' | (string & {}) + r: string + rotate: string + rowGap: string + rx: string + ry: string + scale: string + scrollMargin: string + scrollMarginBlock: string + scrollMarginBlockEnd: string + scrollMarginBlockStart: string + scrollMarginBottom: string + scrollMarginInline: string + scrollMarginInlineEnd: string + scrollMarginInlineStart: string + scrollMarginLeft: string + scrollMarginRight: string + scrollMarginTop: string + scrollPadding: string + scrollPaddingBlock: string + scrollPaddingBlockEnd: string + scrollPaddingBlockStart: string + scrollPaddingBottom: string + scrollPaddingInline: string + scrollPaddingInlineEnd: string + scrollPaddingInlineStart: string + scrollPaddingLeft: string + scrollPaddingRight: string + scrollPaddingTop: string + scrollSnapAlign: 'none' | 'start' | 'end' | 'center' | (string & {}) + scrollSnapStop: 'normal' | 'always' | (string & {}) + scrollbarColor: string + scrollbarGutter: 'auto' | 'stable' | 'stable both-edges' | (string & {}) + scrollbarWidth: 'auto' | 'thin' | 'none' | (string & {}) + shapeRendering: + | 'auto' + | 'optimizeSpeed' + | 'crispEdges' + | 'geometricPrecision' + | (string & {}) + textAutospace: string + textCombineUpright: 'none' | 'all' | (string & {}) + textDecorationSkipInk: 'auto' | 'all' | 'none' | (string & {}) + textDecorationThickness: 'auto' | 'from-font' | (string & {}) + textEmphasis: string + textEmphasisColor: string + textEmphasisPosition: string + textEmphasisStyle: string + textUnderlineOffset: string + textUnderlinePosition: + | 'auto' + | 'from-font' + | 'under' + | 'left' + | 'right' + | (string & {}) + textWrap: 'wrap' | 'nowrap' | 'balance' | 'pretty' | (string & {}) + textWrapMode: 'wrap' | 'nowrap' | (string & {}) + textWrapStyle: 'auto' | 'balance' | 'stable' | 'pretty' | (string & {}) + transformBox: + | 'content-box' + | 'border-box' + | 'fill-box' + | 'stroke-box' + | 'view-box' + | (string & {}) + transitionBehavior: 'normal' | 'allow-discrete' | (string & {}) + translate: string + vectorEffect: + | 'none' + | 'non-scaling-stroke' + | 'non-scaling-size' + | 'non-rotation' + | 'fixed-position' + | (string & {}) + viewTransitionClass: string + viewTransitionName: string + whiteSpaceCollapse: + | 'collapse' + | 'preserve' + | 'preserve-breaks' + | 'preserve-spaces' + | 'break-spaces' + | (string & {}) + x: string + y: string } & Record diff --git a/packages/ts-css/src/keyframes.test.ts b/packages/ts-css/src/keyframes.test.ts new file mode 100644 index 00000000..83be78c3 --- /dev/null +++ b/packages/ts-css/src/keyframes.test.ts @@ -0,0 +1,159 @@ +import prettier from 'prettier' +import { describe, expect, it } from 'vitest' + +import { s } from './s' + +describe('keyframes', () => { + it('renders unprefixed frames, preserves frame order, and puts definitions after selectors', async () => { + const root = s('.notice') + const frames = root.keyframes('fade-in') + frames.select('from').opacity('0') + frames.select('50%, 75.5%').opacity('.5').transform('scale(.9)') + frames.select('to').opacity('1') + root.animation('fade-in 160ms ease-out') + + const expected = `.notice { + animation: fade-in 160ms ease-out; +} + +@keyframes fade-in { + from { + opacity: 0; + } + + 50%, + 75.5% { + opacity: 0.5; + transform: scale(0.9); + } + + to { + opacity: 1; + } +} +` + const raw = root.toCSS() + expect(raw).toContain('@keyframes fade-in {') + expect(raw).toContain('50%, 75.5% {') + expect(raw).not.toContain('.notice from') + expect(raw).not.toContain('.notice @keyframes') + expect(raw).toBe(root.toCSS()) + expect(await prettier.format(raw, { parser: 'css' })).toBe(expected) + expect(await root.toPrettyCSS()).toBe(expected) + }) + + it('reuses blocks and frames without changing revisions until a value changes', () => { + const root = s() + const other = s() + const otherRevision = other.revision + const frames = root.keyframes(' fade-in ') + const from = frames.select('from').opacity('0') + const before = root.revision + + expect(root.keyframes('fade-in')).toBe(frames) + expect(frames.select('from')).toBe(from) + from.opacity('0') + expect(root.revision).toBe(before) + + from.opacity('.2') + expect(root.revision).toBeGreaterThan(before) + expect(other.revision).toBe(otherRevision) + expect(root.toCSS().match(/@keyframes fade-in/g)).toHaveLength(1) + expect(root.toCSS().match(/from \{/g)).toHaveLength(1) + expect(root.toCSS()).toContain('opacity: .2;') + }) + + it('rejects empty names without changing the tree', () => { + const root = s() + const before = root.revision + for (const name of ['', ' ', '\n\t']) { + expect(() => root.keyframes(name)).toThrow( + 'Keyframes name must be a non-empty string.', + ) + } + expect(root.revision).toBe(before) + expect(root.toCSS()).toBe('') + }) + + it('keeps keyframes inside their scope and media blocks', async () => { + const root = s().scope('.hero') + root.keyframes('pulse').select('0%, 100%').opacity('1') + root + .media('prefers-reduced-motion: no-preference') + .select('.packet') + .keyframes('travel') + .select('to') + .set('offset-distance', '100%') + + expect(await root.toPrettyCSS()).toBe(`@scope (.hero) { + @keyframes pulse { + 0%, + 100% { + opacity: 1; + } + } + + @media (prefers-reduced-motion: no-preference) { + @keyframes travel { + to { + offset-distance: 100%; + } + } + } +} +`) + }) + + it('supports hero motion paths, SVG strokes, grouped frames, and reduced motion', async () => { + const root = s() + root.select('.zt-write-packet').css({ + opacity: '0', + 'offset-path': "path('M64 102H118L180 138L280 154')", + 'offset-rotate': 'auto', + animationName: 'zt-write-packet', + }) + const packet = root.keyframes('zt-write-packet') + packet.select('0%').css({ 'offset-distance': '0%', opacity: '0' }) + packet.select('3%').opacity('.9') + packet.select('20%').css({ 'offset-distance': '100%', opacity: '.9' }) + packet.select('23%, 100%').css({ 'offset-distance': '100%', opacity: '0' }) + const trace = root.keyframes('zt-merge-trace') + trace.select('0%, 34%').strokeDashoffset('1').opacity('0') + trace.select('38%').opacity('.8') + trace.select('56%').strokeDashoffset('0').opacity('.8') + trace.select('64%, 100%').strokeDashoffset('0').opacity('0') + root + .select('.zt-engine .zt-motion') + .media('prefers-reduced-motion: reduce') + .animation('none !important') + + const expected = `.zt-write-packet { + opacity: 0; + offset-path: path('M64 102H118L180 138L280 154'); + offset-rotate: auto; + animation-name: zt-write-packet; +} +@media (prefers-reduced-motion: reduce) { + .zt-engine .zt-motion { animation: none !important; } +} +@keyframes zt-write-packet { + 0% { offset-distance: 0%; opacity: 0; } + 3% { opacity: .9; } + 20% { offset-distance: 100%; opacity: .9; } + 23%, 100% { offset-distance: 100%; opacity: 0; } +} +@keyframes zt-merge-trace { + 0%, 34% { stroke-dashoffset: 1; opacity: 0; } + 38% { opacity: .8; } + 56% { stroke-dashoffset: 0; opacity: .8; } + 64%, 100% { stroke-dashoffset: 0; opacity: 0; } +} +` + expect((await root.toPrettyCSS()).replace(/\n\s*\n/g, '\n')).toBe( + (await prettier.format(expected, { parser: 'css' })).replace( + /\n\s*\n/g, + '\n', + ), + ) + }) +}) diff --git a/packages/ts-css/src/meta/cssProps2.ts b/packages/ts-css/src/meta/cssProps2.ts new file mode 100644 index 00000000..1354dcba --- /dev/null +++ b/packages/ts-css/src/meta/cssProps2.ts @@ -0,0 +1,1875 @@ +/** + * Hand-maintained additions to cssProps.ts in the same metadata format. + * Includes the 126 properties selected in docs/css-property-audit.md and the + * seven setters previously maintained directly in Style. Keep existing entries. + * + * Syntax and browser references were checked against: + * https://github.com/microsoft/vscode-custom-data/blob/23b85b992e52a06bebee444bd788ccaa549ae5a1/web-data/data/browsers.css-data.json + */ +export const cssProps2 = [ + { + $: { + name: 'accent-color', + restriction: 'color', + browsers: 'E93,FF92,C93,O79', + ref: 'https://developer.mozilla.org/docs/Web/CSS/accent-color', + syntax: 'auto | ', + }, + desc: 'Sets the accent color of controls that expose an accent color', + values: { + value: [{ $: { name: 'auto' }, desc: 'Use the browser accent color.' }], + }, + }, + { + $: { + name: 'animation-composition', + restriction: 'enum', + browsers: 'E112,FF115,S16,C112,O98', + ref: 'https://developer.mozilla.org/docs/Web/CSS/animation-composition', + syntax: '#', + }, + desc: 'Controls how animation effects combine with underlying property values', + values: { + value: [ + { $: { name: 'replace' }, desc: 'Replace the underlying value.' }, + { $: { name: 'add' }, desc: 'Add the effect to the underlying value.' }, + { + $: { name: 'accumulate' }, + desc: 'Accumulate the effect with the underlying value.', + }, + ], + }, + }, + { + $: { + name: 'appearance', + restriction: 'enum', + browsers: 'E84,FF80,S15.4,C84,O70', + ref: 'https://developer.mozilla.org/docs/Web/CSS/appearance', + syntax: 'none | auto | | ', + }, + desc: 'Controls the native platform appearance of a user interface element', + values: { + value: [ + { $: { name: 'none' }, desc: 'Remove the native appearance.' }, + { + $: { name: 'auto' }, + desc: 'Use the native appearance for the element.', + }, + ], + }, + }, + { + $: { + name: 'aspect-ratio', + restriction: 'ratio', + browsers: 'E88,FF89,S15,C88,O74', + ref: 'https://developer.mozilla.org/docs/Web/CSS/aspect-ratio', + syntax: 'auto || ', + }, + desc: 'Sets the preferred width-to-height ratio used during automatic sizing', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Use the intrinsic ratio when available.', + }, + ], + }, + }, + { + $: { + name: 'background-position-x', + restriction: 'length, percentage', + browsers: 'E12,FF49,S1,C1,O15', + ref: 'https://developer.mozilla.org/docs/Web/CSS/background-position-x', + syntax: + '[ center | [ [ left | right | x-start | x-end ]? ? ]! ]#', + }, + desc: 'Sets the horizontal starting position of each background image', + values: { + value: [ + { $: { name: 'left' }, desc: 'Align with the left edge.' }, + { $: { name: 'center' }, desc: 'Center horizontally.' }, + { $: { name: 'right' }, desc: 'Align with the right edge.' }, + ], + }, + }, + { + $: { + name: 'background-position-y', + restriction: 'length, percentage', + browsers: 'E12,FF49,S1,C1,O15', + ref: 'https://developer.mozilla.org/docs/Web/CSS/background-position-y', + syntax: + '[ center | [ [ top | bottom | y-start | y-end ]? ? ]! ]#', + }, + desc: 'Sets the vertical starting position of each background image', + values: { + value: [ + { $: { name: 'top' }, desc: 'Align with the top edge.' }, + { $: { name: 'center' }, desc: 'Center vertically.' }, + { $: { name: 'bottom' }, desc: 'Align with the bottom edge.' }, + ], + }, + }, + { + $: { + name: 'border-block', + restriction: 'length, color', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-block', + syntax: "<'border-block-start'>", + }, + desc: 'Sets the width, style, and color of both logical block borders', + }, + { + $: { + name: 'border-block-color', + restriction: 'color', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-block-color', + syntax: "<'border-top-color'>{1,2}", + }, + desc: 'Sets the colors of the logical block-start and block-end borders', + }, + { + $: { + name: 'border-block-style', + restriction: 'string', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-block-style', + syntax: "<'border-top-style'>{1,2}", + }, + desc: 'Sets the line styles of the logical block borders', + }, + { + $: { + name: 'border-block-width', + restriction: 'length', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-block-width', + syntax: "<'border-top-width'>{1,2}", + }, + desc: 'Sets the widths of the logical block borders', + }, + { + $: { + name: 'border-end-end-radius', + restriction: 'length, percentage', + browsers: 'E89,FF66,S15,C89,O75', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-end-end-radius', + syntax: "<'border-top-left-radius'>", + }, + desc: 'Rounds the corner at the logical block-end and inline-end edges', + }, + { + $: { + name: 'border-end-start-radius', + restriction: 'length, percentage', + browsers: 'E89,FF66,S15,C89,O75', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-end-start-radius', + syntax: "<'border-top-left-radius'>", + }, + desc: 'Rounds the corner at the logical block-end and inline-start edges', + }, + { + $: { + name: 'border-inline', + restriction: 'length, color', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-inline', + syntax: "<'border-block-start'>", + }, + desc: 'Sets the width, style, and color of both logical inline borders', + }, + { + $: { + name: 'border-inline-color', + restriction: 'color', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-inline-color', + syntax: "<'border-top-color'>{1,2}", + }, + desc: 'Sets the colors of the logical inline-start and inline-end borders', + }, + { + $: { + name: 'border-inline-style', + restriction: 'string', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-inline-style', + syntax: "<'border-top-style'>{1,2}", + }, + desc: 'Sets the line styles of the logical inline borders', + }, + { + $: { + name: 'border-inline-width', + restriction: 'length', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-inline-width', + syntax: "<'border-top-width'>{1,2}", + }, + desc: 'Sets the widths of the logical inline borders', + }, + { + $: { + name: 'border-start-end-radius', + restriction: 'length, percentage', + browsers: 'E89,FF66,S15,C89,O75', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-start-end-radius', + syntax: "<'border-top-left-radius'>", + }, + desc: 'Rounds the corner at the logical block-start and inline-end edges', + }, + { + $: { + name: 'border-start-start-radius', + restriction: 'length, percentage', + browsers: 'E89,FF66,S15,C89,O75', + ref: 'https://developer.mozilla.org/docs/Web/CSS/border-start-start-radius', + syntax: "<'border-top-left-radius'>", + }, + desc: 'Rounds the corner at the logical block-start and inline-start edges', + }, + { + $: { + name: 'break-after', + restriction: 'enum', + browsers: 'E12,FF65,S10,C50,O37', + ref: 'https://developer.mozilla.org/docs/Web/CSS/break-after', + syntax: + 'auto | avoid | always | all | avoid-page | page | left | right | recto | verso | avoid-column | column | avoid-region | region', + }, + desc: 'Controls page, column, or region breaks after the element', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Allow automatic breaks.' }, + { $: { name: 'avoid' }, desc: 'Avoid a break.' }, + { $: { name: 'page' }, desc: 'Force a page break.' }, + { $: { name: 'column' }, desc: 'Force a column break.' }, + ], + }, + }, + { + $: { + name: 'break-before', + restriction: 'enum', + browsers: 'E12,FF65,S10,C50,O37', + ref: 'https://developer.mozilla.org/docs/Web/CSS/break-before', + syntax: + 'auto | avoid | always | all | avoid-page | page | left | right | recto | verso | avoid-column | column | avoid-region | region', + }, + desc: 'Controls page, column, or region breaks before the element', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Allow automatic breaks.' }, + { $: { name: 'avoid' }, desc: 'Avoid a break.' }, + { $: { name: 'page' }, desc: 'Force a page break.' }, + { $: { name: 'column' }, desc: 'Force a column break.' }, + ], + }, + }, + { + $: { + name: 'break-inside', + restriction: 'enum', + browsers: 'E12,FF65,S10,C50,O37', + ref: 'https://developer.mozilla.org/docs/Web/CSS/break-inside', + syntax: 'auto | avoid | avoid-page | avoid-column | avoid-region', + }, + desc: 'Controls whether page, column, or region breaks may occur inside the element', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Allow automatic breaks.' }, + { $: { name: 'avoid' }, desc: 'Avoid breaks inside the element.' }, + { + $: { name: 'avoid-page' }, + desc: 'Avoid page breaks inside the element.', + }, + { + $: { name: 'avoid-column' }, + desc: 'Avoid column breaks inside the element.', + }, + ], + }, + }, + { + $: { + name: 'color-scheme', + restriction: 'string', + browsers: 'E81,FF96,S13,C81,O68', + ref: 'https://developer.mozilla.org/docs/Web/CSS/color-scheme', + syntax: 'normal | [ light | dark | ]+ && only?', + }, + desc: 'Declares the color schemes supported by the element for browser-provided rendering', + values: { + value: [ + { + $: { name: 'normal' }, + desc: 'Use the browser default color scheme.', + }, + { $: { name: 'light' }, desc: 'Support a light color scheme.' }, + { $: { name: 'dark' }, desc: 'Support a dark color scheme.' }, + ], + }, + }, + { + $: { + name: 'column-rule-color', + restriction: 'color', + browsers: 'E12,FF52,S9,C50,O11.1', + ref: 'https://developer.mozilla.org/docs/Web/CSS/column-rule-color', + syntax: '', + }, + desc: 'Sets the color of the rule drawn between columns', + }, + { + $: { + name: 'contain-intrinsic-block-size', + restriction: 'length', + browsers: 'E95,FF107,S17,C95,O81', + ref: 'https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-block-size', + syntax: 'auto? [ none | ]', + }, + desc: 'Provides a placeholder block size when size containment hides the actual content size', + }, + { + $: { + name: 'contain-intrinsic-height', + restriction: 'length', + browsers: 'E95,FF107,S17,C95,O81', + ref: 'https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-height', + syntax: 'auto? [ none | ]', + }, + desc: 'Provides a placeholder height for size-contained content', + }, + { + $: { + name: 'contain-intrinsic-inline-size', + restriction: 'length', + browsers: 'E95,FF107,S17,C95,O81', + ref: 'https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-inline-size', + syntax: 'auto? [ none | ]', + }, + desc: 'Provides a placeholder inline size for size-contained content', + }, + { + $: { + name: 'contain-intrinsic-size', + restriction: 'length', + browsers: 'E83,FF107,S17,C83,O69', + ref: 'https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-size', + syntax: '[ auto? [ none | ] ]{1,2}', + }, + desc: 'Sets placeholder width and height values for size-contained content', + }, + { + $: { + name: 'contain-intrinsic-width', + restriction: 'length', + browsers: 'E95,FF107,S17,C95,O81', + ref: 'https://developer.mozilla.org/docs/Web/CSS/contain-intrinsic-width', + syntax: 'auto? [ none | ]', + }, + desc: 'Provides a placeholder width for size-contained content', + }, + { + $: { + name: 'container', + restriction: 'string', + browsers: 'E105,FF110,S16,C105,O91', + ref: 'https://developer.mozilla.org/docs/Web/CSS/container', + syntax: "<'container-name'> [ / <'container-type'> ]?", + }, + desc: 'Sets a query container name and containment type', + }, + { + $: { + name: 'container-name', + restriction: 'identifier', + browsers: 'E105,FF110,S16,C105,O91', + ref: 'https://developer.mozilla.org/docs/Web/CSS/container-name', + syntax: 'none | +', + }, + desc: 'Names the element for use in container queries', + }, + { + $: { + name: 'container-type', + restriction: 'string', + browsers: 'E105,FF110,S16,C105,O91', + ref: 'https://developer.mozilla.org/docs/Web/CSS/container-type', + syntax: 'normal | [ [ size | inline-size ] || scroll-state ]', + }, + desc: 'Establishes the types of container queries supported by the element', + values: { + value: [ + { + $: { name: 'normal' }, + desc: 'Do not establish size containment for queries.', + }, + { $: { name: 'size' }, desc: 'Allow queries on both size axes.' }, + { + $: { name: 'inline-size' }, + desc: 'Allow queries on the inline size.', + }, + { $: { name: 'scroll-state' }, desc: 'Allow scroll-state queries.' }, + ], + }, + }, + { + $: { + name: 'content-visibility', + restriction: 'enum', + browsers: 'E85,FF125,S18,C85,O71', + ref: 'https://developer.mozilla.org/docs/Web/CSS/content-visibility', + syntax: 'visible | auto | hidden', + }, + desc: 'Controls whether the browser renders the contents of an element', + values: { + value: [ + { $: { name: 'visible' }, desc: 'Render the contents normally.' }, + { + $: { name: 'auto' }, + desc: 'Allow rendering work to be skipped for offscreen content.', + }, + { $: { name: 'hidden' }, desc: 'Skip rendering the contents.' }, + ], + }, + }, + { + $: { + name: 'counter-set', + restriction: 'string', + browsers: 'E85,FF68,S17.2,C85,O71', + ref: 'https://developer.mozilla.org/docs/Web/CSS/counter-set', + syntax: '[ ? ]+ | none', + }, + desc: 'Sets the values of named CSS counters', + }, + { + $: { + name: 'cx', + restriction: 'length, percentage', + browsers: 'E79,FF69,S9,C43,O30', + ref: 'https://developer.mozilla.org/docs/Web/CSS/cx', + syntax: ' | ', + }, + desc: 'Sets the horizontal center coordinate of an SVG circle or ellipse', + }, + { + $: { + name: 'cy', + restriction: 'length, percentage', + browsers: 'E79,FF69,S9,C43,O30', + ref: 'https://developer.mozilla.org/docs/Web/CSS/cy', + syntax: ' | ', + }, + desc: 'Sets the vertical center coordinate of an SVG circle or ellipse', + }, + { + $: { + name: 'dominant-baseline', + restriction: 'enum', + browsers: 'E79,FF1,S4,C1,O15', + ref: 'https://developer.mozilla.org/docs/Web/CSS/dominant-baseline', + syntax: + 'auto | text-bottom | alphabetic | ideographic | middle | central | mathematical | hanging | text-top', + }, + desc: 'Selects the dominant baseline used to align text', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Choose the baseline from the writing mode.', + }, + { $: { name: 'alphabetic' }, desc: 'Use the alphabetic baseline.' }, + { $: { name: 'middle' }, desc: 'Use the middle baseline.' }, + { $: { name: 'central' }, desc: 'Use the central baseline.' }, + { $: { name: 'hanging' }, desc: 'Use the hanging baseline.' }, + ], + }, + }, + { + $: { + name: 'font-optical-sizing', + restriction: 'enum', + browsers: 'E17,FF62,S13.1,C79,O66', + ref: 'https://developer.mozilla.org/docs/Web/CSS/font-optical-sizing', + syntax: 'auto | none', + }, + desc: 'Controls automatic optical-size adjustment in fonts that support it', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Adjust optical sizing to the text size.', + }, + { $: { name: 'none' }, desc: 'Disable automatic optical sizing.' }, + ], + }, + }, + { + $: { + name: 'font-palette', + restriction: 'string', + browsers: 'E101,FF107,S15.4,C101,O87', + ref: 'https://developer.mozilla.org/docs/Web/CSS/font-palette', + syntax: 'normal | light | dark | | ', + }, + desc: 'Chooses the color palette used by a color font', + values: { + value: [ + { $: { name: 'normal' }, desc: 'Use the default palette.' }, + { + $: { name: 'light' }, + desc: 'Choose a palette suited to a light background.', + }, + { + $: { name: 'dark' }, + desc: 'Choose a palette suited to a dark background.', + }, + ], + }, + }, + { + $: { + name: 'font-synthesis-small-caps', + restriction: 'enum', + browsers: 'E97,FF111,S16.4,C97,O83', + ref: 'https://developer.mozilla.org/docs/Web/CSS/font-synthesis-small-caps', + syntax: 'auto | none', + }, + desc: 'Controls whether missing small-cap glyphs may be synthesized', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Allow synthetic small caps.' }, + { + $: { name: 'none' }, + desc: 'Use only small-cap glyphs supplied by the font.', + }, + ], + }, + }, + { + $: { + name: 'font-synthesis-style', + restriction: 'enum', + browsers: 'E97,FF111,S16.4,C97,O83', + ref: 'https://developer.mozilla.org/docs/Web/CSS/font-synthesis-style', + syntax: 'auto | none', + }, + desc: 'Controls whether missing italic or oblique font styles may be synthesized', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Allow synthetic font styles.' }, + { $: { name: 'none' }, desc: 'Use only styles supplied by the font.' }, + ], + }, + }, + { + $: { + name: 'font-synthesis-weight', + restriction: 'enum', + browsers: 'E97,FF111,S16.4,C97,O83', + ref: 'https://developer.mozilla.org/docs/Web/CSS/font-synthesis-weight', + syntax: 'auto | none', + }, + desc: 'Controls whether missing bold font weights may be synthesized', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Allow synthetic bold weights.' }, + { $: { name: 'none' }, desc: 'Use only weights supplied by the font.' }, + ], + }, + }, + { + $: { + name: 'font-variation-settings', + restriction: 'string', + browsers: 'E17,FF62,S11,C62,O49', + ref: 'https://developer.mozilla.org/docs/Web/CSS/font-variation-settings', + syntax: 'normal | [ ]#', + }, + desc: 'Sets individual variation-axis values for a variable font', + }, + { + $: { + name: 'hyphenate-character', + restriction: 'string', + browsers: 'E106,FF98,S17,C106,O92', + ref: 'https://developer.mozilla.org/docs/Web/CSS/hyphenate-character', + syntax: 'auto | ', + }, + desc: 'Selects the string displayed at automatic hyphenation breaks', + }, + { + $: { + name: 'inset', + restriction: 'length, percentage', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset', + syntax: "<'top'>{1,4}", + }, + desc: 'Sets the top, right, bottom, and left offsets of a positioned element', + }, + { + $: { + name: 'inset-block', + restriction: 'length, percentage', + browsers: 'E87,FF63,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset-block', + syntax: "<'top'>{1,2}", + }, + desc: 'Sets both logical block offsets of a positioned element', + }, + { + $: { + name: 'inset-block-end', + restriction: 'length, percentage', + browsers: 'E87,FF63,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset-block-end', + syntax: "<'top'>", + }, + desc: 'Sets the logical block-end offset of a positioned element', + }, + { + $: { + name: 'inset-block-start', + restriction: 'length, percentage', + browsers: 'E87,FF63,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset-block-start', + syntax: "<'top'>", + }, + desc: 'Sets the logical block-start offset of a positioned element', + }, + { + $: { + name: 'inset-inline', + restriction: 'length, percentage', + browsers: 'E87,FF63,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset-inline', + syntax: "<'top'>{1,2}", + }, + desc: 'Sets both logical inline offsets of a positioned element', + }, + { + $: { + name: 'inset-inline-end', + restriction: 'length, percentage', + browsers: 'E87,FF63,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset-inline-end', + syntax: "<'top'>", + }, + desc: 'Sets the logical inline-end offset of a positioned element', + }, + { + $: { + name: 'inset-inline-start', + restriction: 'length, percentage', + browsers: 'E87,FF63,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/inset-inline-start', + syntax: "<'top'>", + }, + desc: 'Sets the logical inline-start offset of a positioned element', + }, + { + $: { + name: 'line-clamp', + restriction: 'integer, string', + browsers: 'O15', + ref: 'https://developer.mozilla.org/docs/Web/CSS/line-clamp', + syntax: 'none | ', + }, + desc: 'Limits the number of displayed lines in a block of text', + values: { + value: [ + { $: { name: 'none' }, desc: 'Do not clamp the number of lines.' }, + ], + }, + }, + { + $: { + name: 'margin-block', + restriction: 'length, percentage', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/margin-block', + syntax: "<'margin-top'>{1,2}", + }, + desc: 'Sets the logical block-start and block-end margins', + }, + { + $: { + name: 'margin-inline', + restriction: 'length, percentage', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/margin-inline', + syntax: "<'margin-top'>{1,2}", + }, + desc: 'Sets the logical inline-start and inline-end margins', + }, + { + $: { + name: 'mask', + restriction: 'string', + browsers: 'E120,FF53,S15.4,C120,O106', + ref: 'https://developer.mozilla.org/docs/Web/CSS/mask', + syntax: '#', + }, + desc: 'Sets the mask images, positions, sizing, clipping, and compositing layers', + }, + { + $: { + name: 'mask-clip', + restriction: 'string', + browsers: 'E120,FF53,S15.4,C120,O106', + ref: 'https://developer.mozilla.org/docs/Web/CSS/mask-clip', + syntax: '[ | no-clip ]#', + }, + desc: 'Specifies the area affected by each mask layer', + values: { + value: [ + { $: { name: 'border-box' }, desc: 'Clip the mask to the border box.' }, + { + $: { name: 'padding-box' }, + desc: 'Clip the mask to the padding box.', + }, + { + $: { name: 'content-box' }, + desc: 'Clip the mask to the content box.', + }, + { $: { name: 'no-clip' }, desc: 'Do not clip the mask.' }, + ], + }, + }, + { + $: { + name: 'mask-composite', + restriction: 'string', + browsers: 'E120,FF53,S15.4,C120,O106', + ref: 'https://developer.mozilla.org/docs/Web/CSS/mask-composite', + syntax: '#', + }, + desc: 'Controls how each mask layer combines with the layers below it', + values: { + value: [ + { + $: { name: 'add' }, + desc: 'Add the current layer to the layers below.', + }, + { + $: { name: 'subtract' }, + desc: 'Keep the part outside the layers below.', + }, + { $: { name: 'intersect' }, desc: 'Keep only the overlapping part.' }, + { + $: { name: 'exclude' }, + desc: 'Keep only the non-overlapping parts.', + }, + ], + }, + }, + { + $: { + name: 'math-style', + restriction: 'enum', + browsers: 'E109,FF117,S14.1,C109,O95', + ref: 'https://developer.mozilla.org/docs/Web/CSS/math-style', + syntax: 'normal | compact', + }, + desc: 'Selects normal or compact layout for mathematical expressions', + values: { + value: [ + { $: { name: 'normal' }, desc: 'Use normal mathematical layout.' }, + { $: { name: 'compact' }, desc: 'Use compact mathematical layout.' }, + ], + }, + }, + { + $: { + name: 'offset', + restriction: 'string', + browsers: 'E79,FF72,S16,C55,O42', + ref: 'https://developer.mozilla.org/docs/Web/CSS/offset', + syntax: + "[ <'offset-position'>? [ <'offset-path'> [ <'offset-distance'> || <'offset-rotate'> ]? ]? ]! [ / <'offset-anchor'> ]?", + }, + desc: 'Sets the motion path, distance, rotation, position, and anchor', + }, + { + $: { + name: 'offset-anchor', + restriction: 'position', + browsers: 'E116,FF72,S16,C116,O102', + ref: 'https://developer.mozilla.org/docs/Web/CSS/offset-anchor', + syntax: 'auto | ', + }, + desc: 'Sets the point on the element that follows its motion path', + }, + { + $: { + name: 'offset-distance', + restriction: 'length, percentage', + browsers: 'E79,FF72,S16,C55,O42', + ref: 'https://developer.mozilla.org/docs/Web/CSS/offset-distance', + syntax: '', + }, + desc: 'Sets how far the element has traveled along its motion path', + }, + { + $: { + name: 'offset-path', + restriction: 'string', + browsers: 'E79,FF72,S15.4,C55,O45', + ref: 'https://developer.mozilla.org/docs/Web/CSS/offset-path', + syntax: 'none | || ', + }, + desc: 'Defines the path followed by an element during motion', + }, + { + $: { + name: 'offset-position', + restriction: 'position', + browsers: 'E116,FF122,S16,C116,O102', + ref: 'https://developer.mozilla.org/docs/Web/CSS/offset-position', + syntax: 'normal | auto | ', + }, + desc: 'Sets the initial position used to establish the motion path', + }, + { + $: { + name: 'offset-rotate', + restriction: 'angle', + browsers: 'E79,FF72,S16,C56,O43', + ref: 'https://developer.mozilla.org/docs/Web/CSS/offset-rotate', + syntax: '[ auto | reverse ] || ', + }, + desc: 'Sets the orientation of an element as it follows its motion path', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Follow the direction of the path.' }, + { + $: { name: 'reverse' }, + desc: 'Face opposite the direction of the path.', + }, + ], + }, + }, + { + $: { + name: 'overflow-block', + restriction: 'enum', + browsers: 'E135,FF69,S26,C135,O120', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overflow-block', + syntax: 'visible | hidden | clip | scroll | auto', + }, + desc: 'Controls overflow along the logical block axis', + values: { + value: [ + { $: { name: 'visible' }, desc: 'Allow content to overflow.' }, + { + $: { name: 'hidden' }, + desc: 'Clip overflow while allowing programmatic scrolling.', + }, + { + $: { name: 'clip' }, + desc: 'Clip overflow without creating a scroll container.', + }, + { + $: { name: 'scroll' }, + desc: 'Provide scrolling even without overflow.', + }, + { + $: { name: 'auto' }, + desc: 'Provide scrolling when content overflows.', + }, + ], + }, + }, + { + $: { + name: 'overflow-inline', + restriction: 'enum', + browsers: 'E135,FF69,S26,C135,O120', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overflow-inline', + syntax: 'visible | hidden | clip | scroll | auto', + }, + desc: 'Controls overflow along the logical inline axis', + values: { + value: [ + { $: { name: 'visible' }, desc: 'Allow content to overflow.' }, + { + $: { name: 'hidden' }, + desc: 'Clip overflow while allowing programmatic scrolling.', + }, + { + $: { name: 'clip' }, + desc: 'Clip overflow without creating a scroll container.', + }, + { + $: { name: 'scroll' }, + desc: 'Provide scrolling even without overflow.', + }, + { + $: { name: 'auto' }, + desc: 'Provide scrolling when content overflows.', + }, + ], + }, + }, + { + $: { + name: 'overscroll-behavior', + restriction: 'string', + browsers: 'E18,FF59,S16,C63,O50', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior', + syntax: '[ contain | none | auto ]{1,2}', + }, + desc: 'Controls scroll chaining and boundary effects on both scroll axes', + }, + { + $: { + name: 'overscroll-behavior-block', + restriction: 'enum', + browsers: 'E79,FF73,S16,C77,O64', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-block', + syntax: 'contain | none | auto', + }, + desc: 'Controls scroll chaining and boundary effects on the logical block axis', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Allow scroll chaining and boundary effects.', + }, + { $: { name: 'contain' }, desc: 'Prevent scroll chaining.' }, + { + $: { name: 'none' }, + desc: 'Prevent scroll chaining and boundary effects.', + }, + ], + }, + }, + { + $: { + name: 'overscroll-behavior-inline', + restriction: 'enum', + browsers: 'E79,FF73,S16,C77,O64', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-inline', + syntax: 'contain | none | auto', + }, + desc: 'Controls scroll chaining and boundary effects on the logical inline axis', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Allow scroll chaining and boundary effects.', + }, + { $: { name: 'contain' }, desc: 'Prevent scroll chaining.' }, + { + $: { name: 'none' }, + desc: 'Prevent scroll chaining and boundary effects.', + }, + ], + }, + }, + { + $: { + name: 'overscroll-behavior-x', + restriction: 'enum', + browsers: 'E18,FF59,S16,C63,O50', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-x', + syntax: 'contain | none | auto', + }, + desc: 'Controls horizontal scroll chaining and boundary effects', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Allow scroll chaining and boundary effects.', + }, + { $: { name: 'contain' }, desc: 'Prevent scroll chaining.' }, + { + $: { name: 'none' }, + desc: 'Prevent scroll chaining and boundary effects.', + }, + ], + }, + }, + { + $: { + name: 'overscroll-behavior-y', + restriction: 'enum', + browsers: 'E18,FF59,S16,C63,O50', + ref: 'https://developer.mozilla.org/docs/Web/CSS/overscroll-behavior-y', + syntax: 'contain | none | auto', + }, + desc: 'Controls vertical scroll chaining and boundary effects', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Allow scroll chaining and boundary effects.', + }, + { $: { name: 'contain' }, desc: 'Prevent scroll chaining.' }, + { + $: { name: 'none' }, + desc: 'Prevent scroll chaining and boundary effects.', + }, + ], + }, + }, + { + $: { + name: 'padding-block', + restriction: 'length, percentage', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/padding-block', + syntax: "<'padding-top'>{1,2}", + }, + desc: 'Sets the logical block-start and block-end padding', + }, + { + $: { + name: 'padding-inline', + restriction: 'length, percentage', + browsers: 'E87,FF66,S14.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/padding-inline', + syntax: "<'padding-top'>{1,2}", + }, + desc: 'Sets the logical inline-start and inline-end padding', + }, + { + $: { + name: 'page', + restriction: 'identifier', + browsers: 'E85,FF110,S1,C85,O71', + ref: 'https://developer.mozilla.org/docs/Web/CSS/page', + syntax: 'auto | ', + }, + desc: 'Assigns the element to a named page in paged media', + }, + { + $: { + name: 'place-content', + restriction: 'string', + browsers: 'E79,FF45,S9,C59,O46', + ref: 'https://developer.mozilla.org/docs/Web/CSS/place-content', + syntax: "<'align-content'> <'justify-content'>?", + }, + desc: 'Sets content alignment on the block and inline axes', + }, + { + $: { + name: 'place-items', + restriction: 'string', + browsers: 'E79,FF45,S11,C59,O46', + ref: 'https://developer.mozilla.org/docs/Web/CSS/place-items', + syntax: "<'align-items'> <'justify-items'>?", + }, + desc: 'Sets item alignment on the block and inline axes', + }, + { + $: { + name: 'place-self', + restriction: 'string', + browsers: 'E79,FF45,S11,C59,O46', + ref: 'https://developer.mozilla.org/docs/Web/CSS/place-self', + syntax: "<'align-self'> <'justify-self'>?", + }, + desc: 'Sets the alignment of an individual item on both axes', + }, + { + $: { + name: 'print-color-adjust', + restriction: 'enum', + browsers: 'E136,FF97,S15.4,C136,O121', + ref: 'https://developer.mozilla.org/docs/Web/CSS/print-color-adjust', + syntax: 'economy | exact', + }, + desc: 'Controls whether the browser may adjust colors when printing', + values: { + value: [ + { + $: { name: 'economy' }, + desc: 'Allow adjustments that reduce printing cost.', + }, + { + $: { name: 'exact' }, + desc: 'Request that authored colors be preserved.', + }, + ], + }, + }, + { + $: { + name: 'r', + restriction: 'length, percentage', + browsers: 'E79,FF69,S9,C43,O30', + ref: 'https://developer.mozilla.org/docs/Web/CSS/r', + syntax: ' | ', + }, + desc: 'Sets the radius of an SVG circle', + }, + { + $: { + name: 'rotate', + restriction: 'angle', + browsers: 'E104,FF72,S14.1,C104,O90', + ref: 'https://developer.mozilla.org/docs/Web/CSS/rotate', + syntax: 'none | | [ x | y | z | {3} ] && ', + }, + desc: 'Sets rotation independently of the transform property', + }, + { + $: { + name: 'row-gap', + restriction: 'length, percentage', + browsers: 'E16,FF52,S10.1,C47,O34', + ref: 'https://developer.mozilla.org/docs/Web/CSS/row-gap', + syntax: 'normal | ', + }, + desc: 'Sets the gap between rows in grid, flex, and multi-column layouts', + }, + { + $: { + name: 'rx', + restriction: 'length, percentage', + browsers: 'E79,FF69,S17.4,C43,O30', + ref: 'https://developer.mozilla.org/docs/Web/CSS/rx', + syntax: ' | ', + }, + desc: 'Sets the horizontal radius of an SVG ellipse or rounded rectangle', + }, + { + $: { + name: 'ry', + restriction: 'length, percentage', + browsers: 'E79,FF69,S17.4,C43,O30', + ref: 'https://developer.mozilla.org/docs/Web/CSS/ry', + syntax: ' | ', + }, + desc: 'Sets the vertical radius of an SVG ellipse or rounded rectangle', + }, + { + $: { + name: 'scale', + restriction: 'number, percentage', + browsers: 'E104,FF72,S14.1,C104,O90', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scale', + syntax: 'none | [ | ]{1,3}', + }, + desc: 'Sets scaling independently of the transform property', + }, + { + $: { + name: 'scroll-margin', + restriction: 'length', + browsers: 'E79,FF90,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin', + syntax: '{1,4}', + }, + desc: 'Sets the outsets of the scroll snap area on all four sides', + }, + { + $: { + name: 'scroll-margin-block', + restriction: 'length', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block', + syntax: '{1,2}', + }, + desc: 'Sets the scroll snap area outsets on both logical block edges', + }, + { + $: { + name: 'scroll-margin-block-end', + restriction: 'length', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block-end', + syntax: '', + }, + desc: 'Sets the scroll snap area outset on the logical block-end edge', + }, + { + $: { + name: 'scroll-margin-block-start', + restriction: 'length', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-block-start', + syntax: '', + }, + desc: 'Sets the scroll snap area outset on the logical block-start edge', + }, + { + $: { + name: 'scroll-margin-bottom', + restriction: 'length', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-bottom', + syntax: '', + }, + desc: 'Sets the bottom outset of the scroll snap area', + }, + { + $: { + name: 'scroll-margin-inline', + restriction: 'length', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline', + syntax: '{1,2}', + }, + desc: 'Sets the scroll snap area outsets on both logical inline edges', + }, + { + $: { + name: 'scroll-margin-inline-end', + restriction: 'length', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline-end', + syntax: '', + }, + desc: 'Sets the scroll snap area outset on the logical inline-end edge', + }, + { + $: { + name: 'scroll-margin-inline-start', + restriction: 'length', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-inline-start', + syntax: '', + }, + desc: 'Sets the scroll snap area outset on the logical inline-start edge', + }, + { + $: { + name: 'scroll-margin-left', + restriction: 'length', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-left', + syntax: '', + }, + desc: 'Sets the left outset of the scroll snap area', + }, + { + $: { + name: 'scroll-margin-right', + restriction: 'length', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-right', + syntax: '', + }, + desc: 'Sets the right outset of the scroll snap area', + }, + { + $: { + name: 'scroll-margin-top', + restriction: 'length', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-margin-top', + syntax: '', + }, + desc: 'Sets the top outset of the scroll snap area', + }, + { + $: { + name: 'scroll-padding', + restriction: 'length, percentage', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding', + syntax: '[ auto | ]{1,4}', + }, + desc: 'Sets the optimal viewing area insets on all four sides of a scrollport', + }, + { + $: { + name: 'scroll-padding-block', + restriction: 'length, percentage', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block', + syntax: '[ auto | ]{1,2}', + }, + desc: 'Sets the scrollport viewing area insets on both logical block edges', + }, + { + $: { + name: 'scroll-padding-block-end', + restriction: 'length, percentage', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block-end', + syntax: 'auto | ', + }, + desc: 'Sets the scrollport viewing area inset on the logical block-end edge', + }, + { + $: { + name: 'scroll-padding-block-start', + restriction: 'length, percentage', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-block-start', + syntax: 'auto | ', + }, + desc: 'Sets the scrollport viewing area inset on the logical block-start edge', + }, + { + $: { + name: 'scroll-padding-bottom', + restriction: 'length, percentage', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-bottom', + syntax: 'auto | ', + }, + desc: 'Sets the bottom inset of the scrollport viewing area', + }, + { + $: { + name: 'scroll-padding-inline', + restriction: 'length, percentage', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline', + syntax: '[ auto | ]{1,2}', + }, + desc: 'Sets the scrollport viewing area insets on both logical inline edges', + }, + { + $: { + name: 'scroll-padding-inline-end', + restriction: 'length, percentage', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline-end', + syntax: 'auto | ', + }, + desc: 'Sets the scrollport viewing area inset on the logical inline-end edge', + }, + { + $: { + name: 'scroll-padding-inline-start', + restriction: 'length, percentage', + browsers: 'E79,FF68,S15,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-inline-start', + syntax: 'auto | ', + }, + desc: 'Sets the scrollport viewing area inset on the logical inline-start edge', + }, + { + $: { + name: 'scroll-padding-left', + restriction: 'length, percentage', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-left', + syntax: 'auto | ', + }, + desc: 'Sets the left inset of the scrollport viewing area', + }, + { + $: { + name: 'scroll-padding-right', + restriction: 'length, percentage', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-right', + syntax: 'auto | ', + }, + desc: 'Sets the right inset of the scrollport viewing area', + }, + { + $: { + name: 'scroll-padding-top', + restriction: 'length, percentage', + browsers: 'E79,FF68,S14.1,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-padding-top', + syntax: 'auto | ', + }, + desc: 'Sets the top inset of the scrollport viewing area', + }, + { + $: { + name: 'scroll-snap-align', + restriction: 'string', + browsers: 'E79,FF68,S11,C69,O56', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-snap-align', + syntax: '[ none | start | end | center ]{1,2}', + }, + desc: 'Sets the alignment of the element within its scroll snap area', + values: { + value: [ + { $: { name: 'none' }, desc: 'Do not define a snap position.' }, + { $: { name: 'start' }, desc: 'Align with the start of the snapport.' }, + { $: { name: 'end' }, desc: 'Align with the end of the snapport.' }, + { + $: { name: 'center' }, + desc: 'Align with the center of the snapport.', + }, + ], + }, + }, + { + $: { + name: 'scroll-snap-stop', + restriction: 'enum', + browsers: 'E79,FF103,S15,C75,O62', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scroll-snap-stop', + syntax: 'normal | always', + }, + desc: 'Controls whether scrolling may pass over a snap position', + values: { + value: [ + { + $: { name: 'normal' }, + desc: 'Allow scrolling to pass over the snap position.', + }, + { + $: { name: 'always' }, + desc: 'Require scrolling to stop at the snap position.', + }, + ], + }, + }, + { + $: { + name: 'scrollbar-color', + restriction: 'color', + browsers: 'E121,FF64,S26.2,C121,O107', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scrollbar-color', + syntax: 'auto | {2}', + }, + desc: 'Sets the thumb and track colors of scrollbars', + }, + { + $: { + name: 'scrollbar-gutter', + restriction: 'string', + browsers: 'E94,FF97,S18.2,C94,O80', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scrollbar-gutter', + syntax: 'auto | stable && both-edges?', + }, + desc: 'Reserves space for scrollbars to prevent layout changes', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Reserve space only when required by the scrollbar.', + }, + { $: { name: 'stable' }, desc: 'Reserve a stable scrollbar gutter.' }, + { + $: { name: 'stable both-edges' }, + desc: 'Reserve matching gutters on both inline edges.', + }, + ], + }, + }, + { + $: { + name: 'scrollbar-width', + restriction: 'enum', + browsers: 'E121,FF64,S18.2,C121,O107', + ref: 'https://developer.mozilla.org/docs/Web/CSS/scrollbar-width', + syntax: 'auto | thin | none', + }, + desc: 'Chooses the thickness of scrollbars', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Use the platform default thickness.' }, + { $: { name: 'thin' }, desc: 'Use a thinner scrollbar.' }, + { + $: { name: 'none' }, + desc: 'Hide the scrollbar without disabling scrolling.', + }, + ], + }, + }, + { + $: { + name: 'shape-rendering', + restriction: 'enum', + browsers: 'E79,FF3,S4,C1,O15', + ref: 'https://developer.mozilla.org/docs/Web/CSS/shape-rendering', + syntax: 'auto | optimizeSpeed | crispEdges | geometricPrecision', + }, + desc: 'Provides rendering-quality hints for SVG shapes', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Let the renderer balance speed and quality.', + }, + { $: { name: 'optimizeSpeed' }, desc: 'Favor rendering speed.' }, + { $: { name: 'crispEdges' }, desc: 'Favor crisp edges.' }, + { + $: { name: 'geometricPrecision' }, + desc: 'Favor geometric accuracy.', + }, + ], + }, + }, + { + $: { + name: 'text-autospace', + restriction: 'string', + browsers: 'E140,FF145,S18.4,C140,O124', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-autospace', + syntax: 'normal | | auto', + }, + desc: 'Controls automatic spacing between characters from different writing systems', + }, + { + $: { + name: 'text-combine-upright', + restriction: 'string', + browsers: 'E79,FF48,S15.4,C48,O35', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-combine-upright', + syntax: 'none | all | [ digits ? ]', + }, + desc: 'Combines a sequence of characters into one upright unit in vertical text', + values: { + value: [ + { $: { name: 'none' }, desc: 'Do not combine characters.' }, + { $: { name: 'all' }, desc: 'Combine all characters in the element.' }, + ], + }, + }, + { + $: { + name: 'text-decoration-skip-ink', + restriction: 'enum', + browsers: 'E79,FF70,S15.4,C64,O50', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-decoration-skip-ink', + syntax: 'auto | all | none', + }, + desc: 'Controls whether text decoration lines avoid glyph strokes', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Let the browser avoid glyph strokes.' }, + { $: { name: 'all' }, desc: 'Avoid glyph strokes wherever possible.' }, + { $: { name: 'none' }, desc: 'Draw continuous decoration lines.' }, + ], + }, + }, + { + $: { + name: 'text-decoration-thickness', + restriction: 'length, percentage', + browsers: 'E89,FF70,S12.1,C89,O75', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-decoration-thickness', + syntax: 'auto | from-font | | ', + }, + desc: 'Sets the thickness of text decoration lines', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Let the browser choose a thickness.' }, + { + $: { name: 'from-font' }, + desc: 'Use the thickness specified by the font.', + }, + ], + }, + }, + { + $: { + name: 'text-emphasis', + restriction: 'string, color', + browsers: 'E99,FF46,S7,C99,O85', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-emphasis', + syntax: "<'text-emphasis-style'> || <'text-emphasis-color'>", + }, + desc: 'Sets the style and color of emphasis marks beside text', + }, + { + $: { + name: 'text-emphasis-color', + restriction: 'color', + browsers: 'E99,FF46,S7,C99,O85', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-emphasis-color', + syntax: '', + }, + desc: 'Sets the color of text emphasis marks', + }, + { + $: { + name: 'text-emphasis-position', + restriction: 'string', + browsers: 'E99,FF46,S7,C99,O85', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-emphasis-position', + syntax: 'auto | [ over | under ] && [ right | left ]?', + }, + desc: 'Positions emphasis marks relative to the text', + }, + { + $: { + name: 'text-emphasis-style', + restriction: 'string', + browsers: 'E99,FF46,S7,C99,O85', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-emphasis-style', + syntax: + 'none | [ [ filled | open ] || [ dot | circle | double-circle | triangle | sesame ] ] | ', + }, + desc: 'Selects the shape and fill of text emphasis marks', + }, + { + $: { + name: 'text-underline-offset', + restriction: 'length, percentage', + browsers: 'E87,FF70,S12.1,C87,O73', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-underline-offset', + syntax: 'auto | | ', + }, + desc: 'Sets the distance between an underline and its original position', + }, + { + $: { + name: 'text-underline-position', + restriction: 'string', + browsers: 'E12,FF74,S12.1,C33,O20', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-underline-position', + syntax: 'auto | from-font | [ under || [ left | right ] ]', + }, + desc: 'Selects the placement of underlines in horizontal and vertical text', + values: { + value: [ + { $: { name: 'auto' }, desc: 'Let the browser choose the position.' }, + { + $: { name: 'from-font' }, + desc: 'Use the underline position supplied by the font.', + }, + { + $: { name: 'under' }, + desc: 'Place the underline below the text content.', + }, + { + $: { name: 'left' }, + desc: 'Place the underline to the left in vertical text.', + }, + { + $: { name: 'right' }, + desc: 'Place the underline to the right in vertical text.', + }, + ], + }, + }, + { + $: { + name: 'text-wrap', + restriction: 'string', + browsers: 'E114,FF121,S17.4,C114,O100', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-wrap', + syntax: "<'text-wrap-mode'> || <'text-wrap-style'>", + }, + desc: 'Sets text wrapping behavior and the strategy used to choose line breaks', + values: { + value: [ + { $: { name: 'wrap' }, desc: 'Allow text to wrap.' }, + { $: { name: 'nowrap' }, desc: 'Prevent text from wrapping.' }, + { $: { name: 'balance' }, desc: 'Balance line lengths.' }, + { + $: { name: 'pretty' }, + desc: 'Favor a more pleasing arrangement of line breaks.', + }, + ], + }, + }, + { + $: { + name: 'text-wrap-mode', + restriction: 'enum', + browsers: 'E130,FF124,S17.4,C130,O115', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-wrap-mode', + syntax: 'wrap | nowrap', + }, + desc: 'Controls whether text may wrap onto additional lines', + values: { + value: [ + { $: { name: 'wrap' }, desc: 'Allow wrapping.' }, + { $: { name: 'nowrap' }, desc: 'Prevent wrapping.' }, + ], + }, + }, + { + $: { + name: 'text-wrap-style', + restriction: 'enum', + browsers: 'E130,FF124,S17.5,C130,O115', + ref: 'https://developer.mozilla.org/docs/Web/CSS/text-wrap-style', + syntax: 'auto | balance | stable | pretty', + }, + desc: 'Chooses the strategy used to arrange wrapped text lines', + values: { + value: [ + { + $: { name: 'auto' }, + desc: 'Use the browser default wrapping strategy.', + }, + { $: { name: 'balance' }, desc: 'Balance line lengths.' }, + { + $: { name: 'stable' }, + desc: 'Avoid changing earlier lines while editing.', + }, + { + $: { name: 'pretty' }, + desc: 'Favor a more pleasing arrangement of line breaks.', + }, + ], + }, + }, + { + $: { + name: 'transform-box', + restriction: 'enum', + browsers: 'E79,FF55,S11,C64,O51', + ref: 'https://developer.mozilla.org/docs/Web/CSS/transform-box', + syntax: 'content-box | border-box | fill-box | stroke-box | view-box', + }, + desc: 'Selects the reference box used by transforms and transform-origin', + values: { + value: [ + { $: { name: 'content-box' }, desc: 'Use the content box.' }, + { $: { name: 'border-box' }, desc: 'Use the border box.' }, + { $: { name: 'fill-box' }, desc: 'Use the object bounding box.' }, + { $: { name: 'stroke-box' }, desc: 'Use the stroke bounding box.' }, + { $: { name: 'view-box' }, desc: 'Use the nearest SVG viewport.' }, + ], + }, + }, + { + $: { + name: 'transition-behavior', + restriction: 'string', + browsers: 'E117,FF129,S17.4,C117,O103', + ref: 'https://developer.mozilla.org/docs/Web/CSS/transition-behavior', + syntax: '#', + }, + desc: 'Controls whether transitions may start for discrete properties', + values: { + value: [ + { + $: { name: 'normal' }, + desc: 'Transition only continuously animatable properties.', + }, + { + $: { name: 'allow-discrete' }, + desc: 'Allow transitions of discrete properties.', + }, + ], + }, + }, + { + $: { + name: 'translate', + restriction: 'length, percentage', + browsers: 'E104,FF72,S14.1,C104,O90', + ref: 'https://developer.mozilla.org/docs/Web/CSS/translate', + syntax: 'none | [ ? ]?', + }, + desc: 'Sets translation independently of the transform property', + }, + { + $: { + name: 'vector-effect', + restriction: 'enum', + browsers: 'E79,FF15,S5.1,C6,O15', + ref: 'https://developer.mozilla.org/docs/Web/CSS/vector-effect', + syntax: + 'none | non-scaling-stroke | non-scaling-size | non-rotation | fixed-position', + }, + desc: 'Controls how transformations affect SVG strokes and geometry', + values: { + value: [ + { + $: { name: 'none' }, + desc: 'Apply the normal transformation behavior.', + }, + { + $: { name: 'non-scaling-stroke' }, + desc: 'Keep stroke width independent of scaling.', + }, + { + $: { name: 'non-scaling-size' }, + desc: 'Keep the object size independent of scaling.', + }, + { + $: { name: 'non-rotation' }, + desc: 'Keep the object orientation independent of rotation.', + }, + { + $: { name: 'fixed-position' }, + desc: 'Keep the object position independent of transformations.', + }, + ], + }, + }, + { + $: { + name: 'view-transition-class', + restriction: 'identifier', + browsers: 'E125,FF144,S18.2,C125,O111', + ref: 'https://developer.mozilla.org/docs/Web/CSS/view-transition-class', + syntax: 'none | +', + }, + desc: 'Assigns classes used to style view-transition pseudo-elements', + }, + { + $: { + name: 'view-transition-name', + restriction: 'identifier', + browsers: 'E111,FF144,S18,C111,O97', + ref: 'https://developer.mozilla.org/docs/Web/CSS/view-transition-name', + syntax: 'none | | match-element', + }, + desc: 'Names an element for capture and matching during a view transition', + }, + { + $: { + name: 'white-space-collapse', + restriction: 'enum', + browsers: 'E114,FF124,S17.4,C114,O100', + ref: 'https://developer.mozilla.org/docs/Web/CSS/white-space-collapse', + syntax: + 'collapse | preserve | preserve-breaks | preserve-spaces | break-spaces', + }, + desc: 'Controls how whitespace characters and segment breaks are collapsed', + values: { + value: [ + { + $: { name: 'collapse' }, + desc: 'Collapse whitespace sequences and segment breaks.', + }, + { + $: { name: 'preserve' }, + desc: 'Preserve whitespace sequences and segment breaks.', + }, + { + $: { name: 'preserve-breaks' }, + desc: 'Preserve segment breaks while collapsing spaces.', + }, + { + $: { name: 'preserve-spaces' }, + desc: 'Preserve spaces while converting tabs and breaks to spaces.', + }, + { + $: { name: 'break-spaces' }, + desc: 'Preserve spaces and allow a break after each one.', + }, + ], + }, + }, + { + $: { + name: 'x', + restriction: 'length, percentage', + browsers: 'E79,FF69,S9,C42,O29', + ref: 'https://developer.mozilla.org/docs/Web/CSS/x', + syntax: ' | ', + }, + desc: 'Sets the horizontal position of an SVG geometry element', + }, + { + $: { + name: 'y', + restriction: 'length, percentage', + browsers: 'E79,FF69,S9,C42,O29', + ref: 'https://developer.mozilla.org/docs/Web/CSS/y', + syntax: ' | ', + }, + desc: 'Sets the vertical position of an SVG geometry element', + }, +] diff --git a/packages/ts-css/src/meta/generateStyle.ts b/packages/ts-css/src/meta/generateStyle.ts index 2144323d..8ff00c56 100644 --- a/packages/ts-css/src/meta/generateStyle.ts +++ b/packages/ts-css/src/meta/generateStyle.ts @@ -6,6 +6,9 @@ import { fileURLToPath } from 'node:url' import prettier from 'prettier' import { cssProps } from './cssProps' +import { cssProps2 } from './cssProps2' + +const allCssProps = [...cssProps, ...cssProps2] const camelize = (s: string) => (s.startsWith('-') ? s.substring(1) : s).replace(/-./g, (x) => @@ -62,7 +65,7 @@ function createCode() { const methods = createMethods() const code = ` import { BaseStyle } from './baseStyle' -import { CSSProps } from './cssProps' +import type { CSSProps } from './cssProps' export class Style extends BaseStyle