Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/features/site-import.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

The static-site pipeline has two parts: a pure analysis function (`buildImportPlan`) that produces an `ImportPlan` preview, and an async commit function (`commitImportPlan`) that uploads assets and writes to the store. CMS bundle imports keep their native semantics: validate the `SiteBundle`, preview against `/admin/api/cms/import/preview`, resolve any row slug conflicts in the shared Conflicts step, then apply through `/admin/api/cms/import` or `/admin/api/cms/import/archive`. The modal uses the same Review category navigator and Import progress surface for CMS bundles, so tables, media, folders, redirects, conflict resolution, and completion all live in the same picker pattern as HTML/CSS/media imports.

CSS declarations containing `var()`, `env()`, `clamp()`, `min()`, or `max()` pass through the shared CSS substitution encoder before CSSOM parsing. This preserves their authored values in both browser and headless imports, including centered section padding and responsive wrapper widths. The decoder restores the original property names before storage.

---

## TL;DR
Expand Down
39 changes: 39 additions & 0 deletions src/__tests__/siteImport/cssToStyleRules-substitution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,45 @@ import {
} from '@core/css-substitution'

describe('cssToStyleRules — substitution declarations survive verbatim', () => {
it('preserves centered section padding and constrained wrappers using min/max', () => {
const { rules, warnings } = cssToStyleRules(`
.inner-page-wrap section { padding: 70px max(24px, calc((100% - 1120px)/2)); }
.wrapper { width: min(100% - 48px, 1160px); }
@media (max-width: 620px) {
.inner-page-wrap section { padding: 54px 20px; }
.wrapper { width: min(100% - 28px, 1220px); }
}
`)
const section = rules.find((rule) => rule.selector === '.inner-page-wrap section')!
expect(section.styles.padding).toBe('70px max(24px, calc((100% - 1120px)/2))')
expect(rules.find((rule) => rule.selector === '.wrapper')!.styles.width)
.toBe('min(100% - 48px, 1160px)')
expect(Object.values(section.contextStyles ?? {})[0]).toMatchObject({
paddingTop: '54px', paddingRight: '20px', paddingBottom: '54px', paddingLeft: '20px',
})
const wrapper = rules.find((rule) => rule.selector === '.wrapper')!
expect(Object.values(wrapper.contextStyles ?? {})[0]).toMatchObject({
width: 'min(100% - 28px, 1220px)',
})
expect(warnings).toHaveLength(0)
})

it('preserves clamp declarations used for fluid typography and spacing', () => {
const { rules, warnings } = cssToStyleRules(`
section { padding: clamp(4rem, 8vw, 9rem) 0; }
.section-header { margin-bottom: clamp(3rem, 5vw, 5rem); }
.track-card { padding: clamp(2rem, 4vw, 3.5rem); }
h2 { font-size: clamp(2.5rem, 5vw, 5rem); }
`)

const bySel = Object.fromEntries(rules.map((r) => [r.selector, r.styles]))
expect(bySel.section.padding).toBe('clamp(4rem, 8vw, 9rem) 0')
expect(bySel['.section-header'].marginBottom).toBe('clamp(3rem, 5vw, 5rem)')
expect(bySel['.track-card'].padding).toBe('clamp(2rem, 4vw, 3.5rem)')
expect(bySel.h2.fontSize).toBe('clamp(2.5rem, 5vw, 5rem)')
expect(warnings).toHaveLength(0)
})

it('preserves shorthand+var declarations byte-faithfully', () => {
const { rules, warnings } = cssToStyleRules(`
.plan { padding: 40px; border-left: 1px solid var(--rule); }
Expand Down
7 changes: 4 additions & 3 deletions src/core/css-substitution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* ## The problem
*
* A declaration whose value contains `var()`/`env()` cannot be expanded at
* parse time, and engines disagree about what their CSSOM then exposes:
* parse time. Headless CSSOM also drops valid `clamp()`, `min()`, and `max()`
* declarations. Engines disagree about what their CSSOM exposes:
*
* - **Chromium** stores a "pending-substitution value": `style.length`
* enumerates the shorthand's longhands, but `getPropertyValue(longhand)`
Expand Down Expand Up @@ -44,8 +45,8 @@ import type { CSSDeclarationPriorityBag } from '@core/page-tree'
/** Prefix for encoded substitution declarations. */
export const SUBSTITUTION_PROP_MARKER = '--instatic-sub-'

/** A value that contains a `var(` or `env(` substitution function. */
export const SUBSTITUTION_FN_RE = /\b(?:var|env)\(/
/** CSS functions whose declarations must survive differing CSSOM implementations. */
export const SUBSTITUTION_FN_RE = /\b(?:var|env|clamp|min|max)\(/i

/** At-rule blocks whose contents must pass through unencoded. */
const SKIPPED_AT_RULES = new Set(['keyframes', 'font-face'])
Expand Down
Loading