@homebound/truss 2.15.0 → 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -544,7 +544,7 @@ var RELATIONSHIP_BASE = {
544
544
  siblingAfter: 40
545
545
  };
546
546
  function computeRulePriority(rule) {
547
- let priority = getPropertyPriority(rule.cssProperty);
547
+ let priority = getPropertyPriority(rule.declarations[0].cssProperty);
548
548
  if (rule.pseudoElement) {
549
549
  priority += PSEUDO_ELEMENT_PRIORITY;
550
550
  }
@@ -565,10 +565,7 @@ function computeRulePriority(rule) {
565
565
  return priority;
566
566
  }
567
567
  function isVariableRule(rule) {
568
- if (rule.declarations) {
569
- return rule.declarations.some((d) => d.cssVarName !== void 0);
570
- }
571
- return rule.cssVarName !== void 0;
568
+ return rule.declarations.some((d) => d.cssVarName !== void 0);
572
569
  }
573
570
  function sortRulesByPriority(rules) {
574
571
  const decorated = rules.map((rule, i) => {
@@ -1013,6 +1010,27 @@ function whenPrefix(whenPseudo) {
1013
1010
  const markerPart = whenPseudo.markerNode?.type === "Identifier" ? `${whenPseudo.markerNode.name}_` : "";
1014
1011
  return `wh_${rel}_${pseudoTag}_${markerPart}`;
1015
1012
  }
1013
+ function conditionPrefix(pseudoClass, mediaQuery, pseudoElement, breakpoints) {
1014
+ const parts = [];
1015
+ if (pseudoElement) {
1016
+ parts.push(`${pseudoElement.replace(/^::/, "")}_`);
1017
+ }
1018
+ if (mediaQuery && breakpoints) {
1019
+ const bpKey = Object.entries(breakpoints).find(([, v]) => v === mediaQuery)?.[0];
1020
+ if (bpKey) {
1021
+ const shortName = bpKey.replace(/^if/, "").toLowerCase();
1022
+ parts.push(`${shortName}_`);
1023
+ } else {
1024
+ parts.push("mq_");
1025
+ }
1026
+ } else if (mediaQuery) {
1027
+ parts.push("mq_");
1028
+ }
1029
+ if (pseudoClass) {
1030
+ parts.push(`${pseudoSelectorTag(pseudoClass)}_`);
1031
+ }
1032
+ return parts.join("");
1033
+ }
1016
1034
  function pseudoSelectorTag(pseudo) {
1017
1035
  const replaced = pseudo.trim().replace(/::?[a-zA-Z-]+/g, (match) => {
1018
1036
  return `_${pseudoIdentifierTag(match)}_`;
@@ -1036,27 +1054,6 @@ function normalizePseudoIdentifier(pseudo) {
1036
1054
  });
1037
1055
  return `${prefix}${name}`;
1038
1056
  }
1039
- function conditionPrefix(pseudoClass, mediaQuery, pseudoElement, breakpoints) {
1040
- const parts = [];
1041
- if (pseudoElement) {
1042
- parts.push(`${pseudoElement.replace(/^::/, "")}_`);
1043
- }
1044
- if (mediaQuery && breakpoints) {
1045
- const bpKey = Object.entries(breakpoints).find(([, v]) => v === mediaQuery)?.[0];
1046
- if (bpKey) {
1047
- const shortName = bpKey.replace(/^if/, "").toLowerCase();
1048
- parts.push(`${shortName}_`);
1049
- } else {
1050
- parts.push("mq_");
1051
- }
1052
- } else if (mediaQuery) {
1053
- parts.push("mq_");
1054
- }
1055
- if (pseudoClass) {
1056
- parts.push(`${pseudoSelectorTag(pseudoClass)}_`);
1057
- }
1058
- return parts.join("");
1059
- }
1060
1057
  function camelToKebab(s) {
1061
1058
  return s.replace(/^(Webkit|Moz|Ms|O)/, (m) => `-${m.toLowerCase()}`).replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
1062
1059
  }
@@ -1067,6 +1064,9 @@ function cleanValueForClassName(value) {
1067
1064
  }
1068
1065
  return cleaned.replace(/[^a-zA-Z0-9]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
1069
1066
  }
1067
+ function getPropertyAbbreviation(cssProp) {
1068
+ return cssPropertyAbbreviations[cssProp] ?? cssProp;
1069
+ }
1070
1070
  function buildLonghandLookup(mapping) {
1071
1071
  const lookup = /* @__PURE__ */ new Map();
1072
1072
  for (const [abbrev, entry] of Object.entries(mapping.abbreviations)) {
@@ -1091,9 +1091,6 @@ function getLonghandLookup(mapping) {
1091
1091
  }
1092
1092
  return cachedLookup;
1093
1093
  }
1094
- function getPropertyAbbreviation(cssProp) {
1095
- return cssPropertyAbbreviations[cssProp] ?? cssProp;
1096
- }
1097
1094
  function computeStaticBaseName(seg, cssProp, cssValue, isMultiProp, mapping) {
1098
1095
  const abbr = seg.abbr;
1099
1096
  if (seg.argResolved !== void 0) {
@@ -1145,10 +1142,11 @@ function collectAtomicRules(chains, mapping) {
1145
1142
  return { rules, needsMaybeInc };
1146
1143
  }
1147
1144
  function segmentContext(seg, mapping) {
1145
+ const prefix = `${conditionPrefix(seg.pseudoClass, seg.mediaQuery, seg.pseudoElement, mapping.breakpoints)}${seg.whenPseudo ? whenPrefix(seg.whenPseudo) : ""}`;
1148
1146
  if (seg.whenPseudo) {
1149
1147
  const wp = seg.whenPseudo;
1150
1148
  return {
1151
- prefix: whenPrefix(wp),
1149
+ prefix,
1152
1150
  whenSelector: {
1153
1151
  relationship: wp.relationship ?? "ancestor",
1154
1152
  markerClass: markerClassName(wp.markerNode),
@@ -1156,7 +1154,7 @@ function segmentContext(seg, mapping) {
1156
1154
  }
1157
1155
  };
1158
1156
  }
1159
- return { prefix: conditionPrefix(seg.pseudoClass, seg.mediaQuery, seg.pseudoElement, mapping.breakpoints) };
1157
+ return { prefix };
1160
1158
  }
1161
1159
  function baseRuleFields(seg) {
1162
1160
  return {
@@ -1175,9 +1173,8 @@ function collectStaticRules(rules, seg, mapping) {
1175
1173
  if (!rules.has(className)) {
1176
1174
  rules.set(className, {
1177
1175
  className,
1178
- cssProperty: camelToKebab(cssProp),
1179
- cssValue,
1180
- ...!whenSelector && baseRuleFields(seg),
1176
+ declarations: [{ cssProperty: camelToKebab(cssProp), cssValue }],
1177
+ ...baseRuleFields(seg),
1181
1178
  whenSelector
1182
1179
  });
1183
1180
  }
@@ -1193,22 +1190,12 @@ function collectVariableRules(rules, seg, mapping) {
1193
1190
  if (!existingRule) {
1194
1191
  rules.set(className, {
1195
1192
  className,
1196
- cssProperty: declaration.cssProperty,
1197
- cssValue: declaration.cssValue,
1198
1193
  declarations: [declaration],
1199
- cssVarName: varName,
1200
- ...!whenSelector && baseRuleFields(seg),
1194
+ ...baseRuleFields(seg),
1201
1195
  whenSelector
1202
1196
  });
1203
1197
  continue;
1204
1198
  }
1205
- existingRule.declarations ??= [
1206
- {
1207
- cssProperty: existingRule.cssProperty,
1208
- cssValue: existingRule.cssValue,
1209
- cssVarName: existingRule.cssVarName
1210
- }
1211
- ];
1212
1199
  if (!existingRule.declarations.some((entry) => {
1213
1200
  return entry.cssProperty === declaration.cssProperty;
1214
1201
  })) {
@@ -1225,9 +1212,8 @@ function collectVariableRules(rules, seg, mapping) {
1225
1212
  if (!rules.has(extraName)) {
1226
1213
  rules.set(extraName, {
1227
1214
  className: extraName,
1228
- cssProperty: camelToKebab(cssProp),
1229
- cssValue,
1230
- ...!whenSelector && baseRuleFields(seg),
1215
+ declarations: [{ cssProperty: camelToKebab(cssProp), cssValue }],
1216
+ ...baseRuleFields(seg),
1231
1217
  whenSelector
1232
1218
  });
1233
1219
  }
@@ -1256,61 +1242,51 @@ function generateCssText(rules) {
1256
1242
  return lines.join("\n");
1257
1243
  }
1258
1244
  function formatRule(rule) {
1259
- if (rule.whenSelector) return formatWhenRule(rule);
1260
- if (rule.mediaQuery && rule.pseudoClass) return formatMediaPseudoRule(rule);
1261
- if (rule.mediaQuery && rule.pseudoElement) return formatMediaPseudoElementRule(rule);
1262
- if (rule.mediaQuery) return formatMediaRule(rule);
1263
- if (rule.pseudoClass && rule.pseudoElement) return formatPseudoRule(rule);
1264
- if (rule.pseudoElement) return formatPseudoElementRule(rule);
1265
- if (rule.pseudoClass) return formatPseudoRule(rule);
1266
- return formatBaseRule(rule);
1267
- }
1268
- function formatBaseRule(rule) {
1269
- return formatRuleBlock(`.${rule.className}`, rule);
1270
- }
1271
- function formatPseudoRule(rule) {
1272
- const pe = rule.pseudoElement ? rule.pseudoElement : "";
1273
- return formatRuleBlock(`.${rule.className}${rule.pseudoClass}${pe}`, rule);
1274
- }
1275
- function formatPseudoElementRule(rule) {
1276
- return formatRuleBlock(`.${rule.className}${rule.pseudoElement}`, rule);
1277
- }
1278
- function formatWhenRule(rule) {
1279
1245
  const whenSelector = rule.whenSelector;
1280
- if (!whenSelector) {
1281
- return formatBaseRule(rule);
1282
- }
1246
+ if (whenSelector) return formatWhenRule(rule, whenSelector);
1247
+ const selector = buildTargetSelector(rule, !!rule.mediaQuery);
1248
+ return formatRuleWithOptionalMedia(rule, selector);
1249
+ }
1250
+ function formatWhenRule(rule, whenSelector) {
1283
1251
  const markerSelector = `.${whenSelector.markerClass}${whenSelector.pseudo}`;
1284
- const targetSelector = `.${rule.className}`;
1252
+ const duplicateClassName = !!rule.mediaQuery;
1285
1253
  if (whenSelector.relationship === "ancestor") {
1286
- return formatRuleBlock(`${markerSelector} ${targetSelector}`, rule);
1254
+ return formatRuleWithOptionalMedia(rule, `${markerSelector} ${buildTargetSelector(rule, duplicateClassName)}`);
1287
1255
  }
1288
1256
  if (whenSelector.relationship === "descendant") {
1289
- return formatRuleBlock(`${targetSelector}:has(${markerSelector})`, rule);
1257
+ return formatRuleWithOptionalMedia(rule, buildTargetSelector(rule, duplicateClassName, `:has(${markerSelector})`));
1290
1258
  }
1291
1259
  if (whenSelector.relationship === "siblingAfter") {
1292
- return formatRuleBlock(`${targetSelector}:has(~ ${markerSelector})`, rule);
1260
+ return formatRuleWithOptionalMedia(
1261
+ rule,
1262
+ buildTargetSelector(rule, duplicateClassName, `:has(~ ${markerSelector})`)
1263
+ );
1293
1264
  }
1294
1265
  if (whenSelector.relationship === "siblingBefore") {
1295
- return formatRuleBlock(`${markerSelector} ~ ${targetSelector}`, rule);
1266
+ return formatRuleWithOptionalMedia(rule, `${markerSelector} ~ ${buildTargetSelector(rule, duplicateClassName)}`);
1296
1267
  }
1297
1268
  if (whenSelector.relationship === "anySibling") {
1298
- return formatRuleBlock(`${targetSelector}:has(~ ${markerSelector}), ${markerSelector} ~ ${targetSelector}`, rule);
1269
+ const afterSelector = buildTargetSelector(rule, duplicateClassName, `:has(~ ${markerSelector})`);
1270
+ const beforeSelector = `${markerSelector} ~ ${buildTargetSelector(rule, duplicateClassName)}`;
1271
+ return formatRuleWithOptionalMedia(rule, `${afterSelector}, ${beforeSelector}`);
1299
1272
  }
1300
- return formatRuleBlock(`${markerSelector} ${targetSelector}`, rule);
1273
+ return formatRuleWithOptionalMedia(rule, `${markerSelector} ${buildTargetSelector(rule, duplicateClassName)}`);
1301
1274
  }
1302
- function formatMediaRule(rule) {
1303
- return formatNestedRuleBlock(rule.mediaQuery, `.${rule.className}.${rule.className}`, rule);
1275
+ function buildTargetSelector(rule, duplicateClassName, extraPseudoClass) {
1276
+ const classSelector = duplicateClassName ? `.${rule.className}.${rule.className}` : `.${rule.className}`;
1277
+ const pseudoClass = rule.pseudoClass ?? "";
1278
+ const relationshipPseudoClass = extraPseudoClass ?? "";
1279
+ const pseudoElement = rule.pseudoElement ?? "";
1280
+ return `${classSelector}${pseudoClass}${relationshipPseudoClass}${pseudoElement}`;
1304
1281
  }
1305
- function formatMediaPseudoRule(rule) {
1306
- return formatNestedRuleBlock(rule.mediaQuery, `.${rule.className}.${rule.className}${rule.pseudoClass}`, rule);
1307
- }
1308
- function formatMediaPseudoElementRule(rule) {
1309
- const pe = rule.pseudoElement ?? "";
1310
- return formatNestedRuleBlock(rule.mediaQuery, `.${rule.className}.${rule.className}${pe}`, rule);
1282
+ function formatRuleWithOptionalMedia(rule, selector) {
1283
+ if (rule.mediaQuery) {
1284
+ return formatNestedRuleBlock(rule.mediaQuery, selector, rule);
1285
+ }
1286
+ return formatRuleBlock(selector, rule);
1311
1287
  }
1312
1288
  function getRuleDeclarations(rule) {
1313
- return rule.declarations ?? [{ cssProperty: rule.cssProperty, cssValue: rule.cssValue, cssVarName: rule.cssVarName }];
1289
+ return rule.declarations;
1314
1290
  }
1315
1291
  function formatRuleBlock(selector, rule) {
1316
1292
  const body = getRuleDeclarations(rule).map((declaration) => {
@@ -1449,6 +1425,78 @@ import * as t4 from "@babel/types";
1449
1425
  import { basename } from "path";
1450
1426
 
1451
1427
  // src/plugin/resolve-chain.ts
1428
+ function emptyConditionContext() {
1429
+ return {
1430
+ mediaQuery: null,
1431
+ pseudoClass: null,
1432
+ pseudoElement: null,
1433
+ whenPseudo: null
1434
+ };
1435
+ }
1436
+ function cloneConditionContext(context) {
1437
+ return {
1438
+ mediaQuery: context.mediaQuery,
1439
+ pseudoClass: context.pseudoClass,
1440
+ pseudoElement: context.pseudoElement,
1441
+ whenPseudo: context.whenPseudo ? { ...context.whenPseudo } : null
1442
+ };
1443
+ }
1444
+ function segmentWithConditionContext(segment, context) {
1445
+ return {
1446
+ ...segment,
1447
+ mediaQuery: context.mediaQuery,
1448
+ pseudoClass: context.pseudoClass,
1449
+ pseudoElement: context.pseudoElement,
1450
+ whenPseudo: context.whenPseudo
1451
+ };
1452
+ }
1453
+ function applyModifierNodeToConditionContext(context, node, mapping) {
1454
+ if (node.type === "__mediaQuery") {
1455
+ context.mediaQuery = node.mediaQuery;
1456
+ return;
1457
+ }
1458
+ if (node.type === "getter") {
1459
+ if (isPseudoMethod(node.name)) {
1460
+ context.pseudoClass = pseudoSelector(node.name);
1461
+ return;
1462
+ }
1463
+ if (mapping.breakpoints && node.name in mapping.breakpoints) {
1464
+ context.mediaQuery = mapping.breakpoints[node.name];
1465
+ }
1466
+ return;
1467
+ }
1468
+ if (node.type !== "call") {
1469
+ return;
1470
+ }
1471
+ if (node.name === "ifContainer") {
1472
+ try {
1473
+ context.mediaQuery = containerSelectorFromCall(node);
1474
+ } catch {
1475
+ }
1476
+ return;
1477
+ }
1478
+ if (node.name === "element") {
1479
+ if (node.args.length === 1 && node.args[0].type === "StringLiteral") {
1480
+ context.pseudoElement = node.args[0].value;
1481
+ }
1482
+ return;
1483
+ }
1484
+ if (node.name === "when") {
1485
+ try {
1486
+ const resolved = resolveWhenCall(node);
1487
+ if (resolved.kind === "selector") {
1488
+ context.pseudoClass = resolved.pseudo;
1489
+ } else {
1490
+ context.whenPseudo = resolved;
1491
+ }
1492
+ } catch {
1493
+ }
1494
+ return;
1495
+ }
1496
+ if (isPseudoMethod(node.name)) {
1497
+ context.pseudoClass = pseudoSelector(node.name);
1498
+ }
1499
+ }
1452
1500
  function resolveFullChain(chain, mapping) {
1453
1501
  const parts = [];
1454
1502
  const markers = [];
@@ -1470,20 +1518,38 @@ function resolveFullChain(chain, mapping) {
1470
1518
  }
1471
1519
  let i = 0;
1472
1520
  let currentNodes = [];
1521
+ let currentContext = emptyConditionContext();
1522
+ let currentNodesStartContext = emptyConditionContext();
1523
+ function flushCurrentNodes() {
1524
+ if (currentNodes.length === 0) {
1525
+ return;
1526
+ }
1527
+ parts.push({
1528
+ type: "unconditional",
1529
+ segments: resolveChain(currentNodes, mapping, currentNodesStartContext)
1530
+ });
1531
+ currentNodes = [];
1532
+ currentNodesStartContext = cloneConditionContext(currentContext);
1533
+ }
1534
+ function pushCurrentNode(nodeToPush) {
1535
+ if (currentNodes.length === 0) {
1536
+ currentNodesStartContext = cloneConditionContext(currentContext);
1537
+ }
1538
+ currentNodes.push(nodeToPush);
1539
+ applyModifierNodeToConditionContext(currentContext, nodeToPush, mapping);
1540
+ }
1473
1541
  while (i < filteredChain.length) {
1474
1542
  const node = filteredChain[i];
1475
1543
  const mediaStart = getMediaConditionalStartNode(node, mapping);
1476
1544
  if (mediaStart) {
1477
1545
  const elseIndex = findElseIndex(filteredChain, i + 1);
1478
1546
  if (elseIndex !== -1) {
1479
- if (currentNodes.length > 0) {
1480
- parts.push({ type: "unconditional", segments: resolveChain(currentNodes, mapping) });
1481
- currentNodes = [];
1482
- }
1547
+ flushCurrentNodes();
1548
+ const branchContext = cloneConditionContext(currentContext);
1483
1549
  const thenNodes = mediaStart.thenNodes ? [...mediaStart.thenNodes, ...filteredChain.slice(i + 1, elseIndex)] : filteredChain.slice(i, elseIndex);
1484
1550
  const elseNodes = [makeMediaQueryNode(mediaStart.inverseMediaQuery), ...filteredChain.slice(elseIndex + 1)];
1485
- const thenSegs = resolveChain(thenNodes, mapping);
1486
- const elseSegs = resolveChain(elseNodes, mapping);
1551
+ const thenSegs = resolveChain(thenNodes, mapping, branchContext);
1552
+ const elseSegs = resolveChain(elseNodes, mapping, branchContext);
1487
1553
  parts.push({ type: "unconditional", segments: [...thenSegs, ...elseSegs] });
1488
1554
  i = filteredChain.length;
1489
1555
  break;
@@ -1492,14 +1558,12 @@ function resolveFullChain(chain, mapping) {
1492
1558
  if (node.type === "if") {
1493
1559
  if (node.conditionNode.type === "StringLiteral") {
1494
1560
  const mediaQuery = node.conditionNode.value;
1495
- currentNodes.push({ type: "__mediaQuery", mediaQuery });
1561
+ pushCurrentNode({ type: "__mediaQuery", mediaQuery });
1496
1562
  i++;
1497
1563
  continue;
1498
1564
  }
1499
- if (currentNodes.length > 0) {
1500
- parts.push({ type: "unconditional", segments: resolveChain(currentNodes, mapping) });
1501
- currentNodes = [];
1502
- }
1565
+ flushCurrentNodes();
1566
+ const branchContext = cloneConditionContext(currentContext);
1503
1567
  const thenNodes = [];
1504
1568
  const elseNodes = [];
1505
1569
  i++;
@@ -1520,8 +1584,8 @@ function resolveFullChain(chain, mapping) {
1520
1584
  }
1521
1585
  i++;
1522
1586
  }
1523
- const thenSegs = resolveChain(thenNodes, mapping);
1524
- const elseSegs = resolveChain(elseNodes, mapping);
1587
+ const thenSegs = resolveChain(thenNodes, mapping, branchContext);
1588
+ const elseSegs = resolveChain(elseNodes, mapping, branchContext);
1525
1589
  parts.push({
1526
1590
  type: "conditional",
1527
1591
  conditionNode: node.conditionNode,
@@ -1529,13 +1593,11 @@ function resolveFullChain(chain, mapping) {
1529
1593
  elseSegments: elseSegs
1530
1594
  });
1531
1595
  } else {
1532
- currentNodes.push(node);
1596
+ pushCurrentNode(node);
1533
1597
  i++;
1534
1598
  }
1535
1599
  }
1536
- if (currentNodes.length > 0) {
1537
- parts.push({ type: "unconditional", segments: resolveChain(currentNodes, mapping) });
1538
- }
1600
+ flushCurrentNodes();
1539
1601
  const segmentErrors = [];
1540
1602
  for (const part of parts) {
1541
1603
  const segs = part.type === "unconditional" ? part.segments : [...part.thenSegments, ...part.elseSegments];
@@ -1594,29 +1656,23 @@ function invertMediaQuery(query) {
1594
1656
  }
1595
1657
  return query.replace("@media", "@media not");
1596
1658
  }
1597
- function resolveChain(chain, mapping) {
1659
+ function resolveChain(chain, mapping, initialContext = emptyConditionContext()) {
1598
1660
  const segments = [];
1599
- let currentMediaQuery = null;
1600
- let currentPseudoClass = null;
1601
- let currentPseudoElement = null;
1602
- let currentWhenPseudo = null;
1661
+ const context = cloneConditionContext(initialContext);
1603
1662
  for (const node of chain) {
1604
1663
  try {
1605
1664
  if (node.type === "__mediaQuery") {
1606
- currentMediaQuery = node.mediaQuery;
1607
- currentWhenPseudo = null;
1665
+ context.mediaQuery = node.mediaQuery;
1608
1666
  continue;
1609
1667
  }
1610
1668
  if (node.type === "getter") {
1611
1669
  const abbr = node.name;
1612
1670
  if (isPseudoMethod(abbr)) {
1613
- currentPseudoClass = pseudoSelector(abbr);
1614
- currentWhenPseudo = null;
1671
+ context.pseudoClass = pseudoSelector(abbr);
1615
1672
  continue;
1616
1673
  }
1617
1674
  if (mapping.breakpoints && abbr in mapping.breakpoints) {
1618
- currentMediaQuery = mapping.breakpoints[abbr];
1619
- currentWhenPseudo = null;
1675
+ context.mediaQuery = mapping.breakpoints[abbr];
1620
1676
  continue;
1621
1677
  }
1622
1678
  const entry = mapping.abbreviations[abbr];
@@ -1627,27 +1683,26 @@ function resolveChain(chain, mapping) {
1627
1683
  abbr,
1628
1684
  entry,
1629
1685
  mapping,
1630
- currentMediaQuery,
1631
- currentPseudoClass,
1632
- currentPseudoElement,
1633
- currentWhenPseudo
1686
+ context.mediaQuery,
1687
+ context.pseudoClass,
1688
+ context.pseudoElement,
1689
+ context.whenPseudo
1634
1690
  );
1635
1691
  segments.push(...resolved);
1636
1692
  } else if (node.type === "call") {
1637
1693
  const abbr = node.name;
1638
1694
  if (abbr === "ifContainer") {
1639
- currentMediaQuery = containerSelectorFromCall(node);
1640
- currentWhenPseudo = null;
1695
+ context.mediaQuery = containerSelectorFromCall(node);
1641
1696
  continue;
1642
1697
  }
1643
1698
  if (abbr === "add" || abbr === "addCss") {
1644
1699
  const seg = resolveAddCall(
1645
1700
  node,
1646
1701
  mapping,
1647
- currentMediaQuery,
1648
- currentPseudoClass,
1649
- currentPseudoElement,
1650
- currentWhenPseudo
1702
+ context.mediaQuery,
1703
+ context.pseudoClass,
1704
+ context.pseudoElement,
1705
+ context.whenPseudo
1651
1706
  );
1652
1707
  segments.push(seg);
1653
1708
  continue;
@@ -1655,10 +1710,10 @@ function resolveChain(chain, mapping) {
1655
1710
  if (abbr === "className") {
1656
1711
  const seg = resolveClassNameCall(
1657
1712
  node,
1658
- currentMediaQuery,
1659
- currentPseudoClass,
1660
- currentPseudoElement,
1661
- currentWhenPseudo
1713
+ context.mediaQuery,
1714
+ context.pseudoClass,
1715
+ context.pseudoElement,
1716
+ context.whenPseudo
1662
1717
  );
1663
1718
  segments.push(seg);
1664
1719
  continue;
@@ -1667,9 +1722,10 @@ function resolveChain(chain, mapping) {
1667
1722
  const resolved = resolveTypographyCall(
1668
1723
  node,
1669
1724
  mapping,
1670
- currentMediaQuery,
1671
- currentPseudoClass,
1672
- currentPseudoElement
1725
+ context.mediaQuery,
1726
+ context.pseudoClass,
1727
+ context.pseudoElement,
1728
+ context.whenPseudo
1673
1729
  );
1674
1730
  segments.push(...resolved);
1675
1731
  continue;
@@ -1680,24 +1736,20 @@ function resolveChain(chain, mapping) {
1680
1736
  `element() requires exactly one string literal argument (e.g. "::placeholder")`
1681
1737
  );
1682
1738
  }
1683
- currentPseudoElement = node.args[0].value;
1739
+ context.pseudoElement = node.args[0].value;
1684
1740
  continue;
1685
1741
  }
1686
1742
  if (abbr === "when") {
1687
1743
  const resolved = resolveWhenCall(node);
1688
1744
  if (resolved.kind === "selector") {
1689
- currentPseudoClass = resolved.pseudo;
1690
- currentWhenPseudo = null;
1745
+ context.pseudoClass = resolved.pseudo;
1691
1746
  } else {
1692
- currentPseudoClass = null;
1693
- currentMediaQuery = null;
1694
- currentWhenPseudo = resolved;
1747
+ context.whenPseudo = resolved;
1695
1748
  }
1696
1749
  continue;
1697
1750
  }
1698
1751
  if (isPseudoMethod(abbr)) {
1699
- currentPseudoClass = pseudoSelector(abbr);
1700
- currentWhenPseudo = null;
1752
+ context.pseudoClass = pseudoSelector(abbr);
1701
1753
  if (node.args.length > 0) {
1702
1754
  throw new UnsupportedPatternError(
1703
1755
  `${abbr}() does not take arguments -- use when(marker, "ancestor", ":hover") for relationship selectors`
@@ -1715,10 +1767,10 @@ function resolveChain(chain, mapping) {
1715
1767
  entry,
1716
1768
  node,
1717
1769
  mapping,
1718
- currentMediaQuery,
1719
- currentPseudoClass,
1720
- currentPseudoElement,
1721
- currentWhenPseudo
1770
+ context.mediaQuery,
1771
+ context.pseudoClass,
1772
+ context.pseudoElement,
1773
+ context.whenPseudo
1722
1774
  );
1723
1775
  segments.push(seg);
1724
1776
  } else if (entry.kind === "delegate") {
@@ -1727,10 +1779,10 @@ function resolveChain(chain, mapping) {
1727
1779
  entry,
1728
1780
  node,
1729
1781
  mapping,
1730
- currentMediaQuery,
1731
- currentPseudoClass,
1732
- currentPseudoElement,
1733
- currentWhenPseudo
1782
+ context.mediaQuery,
1783
+ context.pseudoClass,
1784
+ context.pseudoElement,
1785
+ context.whenPseudo
1734
1786
  );
1735
1787
  segments.push(seg);
1736
1788
  } else {
@@ -1747,7 +1799,7 @@ function resolveChain(chain, mapping) {
1747
1799
  }
1748
1800
  return segments;
1749
1801
  }
1750
- function typographyLookupKeySuffix(mediaQuery, pseudoClass, pseudoElement, breakpoints) {
1802
+ function typographyLookupKeySuffix(mediaQuery, pseudoClass, pseudoElement, whenPseudo, breakpoints) {
1751
1803
  const parts = [];
1752
1804
  if (pseudoElement) parts.push(pseudoElement.replace(/^::/, ""));
1753
1805
  if (mediaQuery && breakpoints) {
@@ -1757,25 +1809,36 @@ function typographyLookupKeySuffix(mediaQuery, pseudoClass, pseudoElement, break
1757
1809
  parts.push("mq");
1758
1810
  }
1759
1811
  if (pseudoClass) parts.push(pseudoClass.replace(/^:+/, "").replace(/-/g, "_"));
1812
+ if (whenPseudo) parts.push(whenLookupKeyPart(whenPseudo));
1813
+ return parts.join("_");
1814
+ }
1815
+ function whenLookupKeyPart(whenPseudo) {
1816
+ const parts = ["when", whenPseudo.relationship ?? "ancestor", sanitizeLookupToken(whenPseudo.pseudo)];
1817
+ if (whenPseudo.markerNode?.type === "Identifier" && whenPseudo.markerNode.name) {
1818
+ parts.push(whenPseudo.markerNode.name);
1819
+ }
1760
1820
  return parts.join("_");
1761
1821
  }
1762
- function resolveTypographyCall(node, mapping, mediaQuery, pseudoClass, pseudoElement) {
1822
+ function sanitizeLookupToken(value) {
1823
+ return value.replace(/[^a-zA-Z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "") || "value";
1824
+ }
1825
+ function resolveTypographyCall(node, mapping, mediaQuery, pseudoClass, pseudoElement, whenPseudo) {
1763
1826
  if (node.args.length !== 1) {
1764
1827
  throw new UnsupportedPatternError(`typography() expects exactly 1 argument, got ${node.args.length}`);
1765
1828
  }
1766
1829
  const argAst = node.args[0];
1767
1830
  if (argAst.type === "StringLiteral") {
1768
- return resolveTypographyEntry(argAst.value, mapping, mediaQuery, pseudoClass, pseudoElement);
1831
+ return resolveTypographyEntry(argAst.value, mapping, mediaQuery, pseudoClass, pseudoElement, whenPseudo);
1769
1832
  }
1770
1833
  const typography = mapping.typography ?? [];
1771
1834
  if (typography.length === 0) {
1772
1835
  throw new UnsupportedPatternError(`typography() is unavailable because no typography abbreviations were generated`);
1773
1836
  }
1774
- const suffix = typographyLookupKeySuffix(mediaQuery, pseudoClass, pseudoElement, mapping.breakpoints);
1837
+ const suffix = typographyLookupKeySuffix(mediaQuery, pseudoClass, pseudoElement, whenPseudo, mapping.breakpoints);
1775
1838
  const lookupKey = suffix ? `typography__${suffix}` : "typography";
1776
1839
  const segmentsByName = {};
1777
1840
  for (const name of typography) {
1778
- segmentsByName[name] = resolveTypographyEntry(name, mapping, mediaQuery, pseudoClass, pseudoElement);
1841
+ segmentsByName[name] = resolveTypographyEntry(name, mapping, mediaQuery, pseudoClass, pseudoElement, whenPseudo);
1779
1842
  }
1780
1843
  return [
1781
1844
  {
@@ -1789,7 +1852,7 @@ function resolveTypographyCall(node, mapping, mediaQuery, pseudoClass, pseudoEle
1789
1852
  }
1790
1853
  ];
1791
1854
  }
1792
- function resolveTypographyEntry(name, mapping, mediaQuery, pseudoClass, pseudoElement) {
1855
+ function resolveTypographyEntry(name, mapping, mediaQuery, pseudoClass, pseudoElement, whenPseudo) {
1793
1856
  if (!(mapping.typography ?? []).includes(name)) {
1794
1857
  throw new UnsupportedPatternError(`Unknown typography abbreviation "${name}"`);
1795
1858
  }
@@ -1797,21 +1860,24 @@ function resolveTypographyEntry(name, mapping, mediaQuery, pseudoClass, pseudoEl
1797
1860
  if (!entry) {
1798
1861
  throw new UnsupportedPatternError(`Unknown typography abbreviation "${name}"`);
1799
1862
  }
1800
- const resolved = resolveEntry(name, entry, mapping, mediaQuery, pseudoClass, pseudoElement, null);
1863
+ const resolved = resolveEntry(name, entry, mapping, mediaQuery, pseudoClass, pseudoElement, whenPseudo);
1801
1864
  for (const segment of resolved) {
1802
- if (segment.variableProps || segment.whenPseudo) {
1865
+ if (segment.variableProps) {
1803
1866
  throw new UnsupportedPatternError(`Typography abbreviation "${name}" cannot require runtime arguments`);
1804
1867
  }
1805
1868
  }
1806
1869
  return resolved;
1807
1870
  }
1808
1871
  function resolveEntry(abbr, entry, mapping, mediaQuery, pseudoClass, pseudoElement, whenPseudo) {
1872
+ const context = {
1873
+ mediaQuery,
1874
+ pseudoClass,
1875
+ pseudoElement,
1876
+ whenPseudo
1877
+ };
1809
1878
  switch (entry.kind) {
1810
1879
  case "static": {
1811
- if (whenPseudo) {
1812
- return [{ abbr, defs: entry.defs, whenPseudo }];
1813
- }
1814
- return [{ abbr, defs: entry.defs, mediaQuery, pseudoClass, pseudoElement }];
1880
+ return [segmentWithConditionContext({ abbr, defs: entry.defs }, context)];
1815
1881
  }
1816
1882
  case "alias": {
1817
1883
  const result = [];
@@ -1874,40 +1940,32 @@ function resolveDelegateCall(abbr, entry, node, mapping, mediaQuery, pseudoClass
1874
1940
  }
1875
1941
  function buildParameterizedSegment(params) {
1876
1942
  const { abbr, props, incremented, appendPx, extraDefs, argAst, literalValue, whenPseudo } = params;
1943
+ const context = {
1944
+ mediaQuery: params.mediaQuery,
1945
+ pseudoClass: params.pseudoClass,
1946
+ pseudoElement: params.pseudoElement,
1947
+ whenPseudo
1948
+ };
1877
1949
  if (literalValue !== null) {
1878
1950
  const defs = {};
1879
1951
  for (const prop of props) {
1880
1952
  defs[prop] = literalValue;
1881
1953
  }
1882
1954
  if (extraDefs) Object.assign(defs, extraDefs);
1883
- if (whenPseudo) {
1884
- return { abbr, defs, whenPseudo, argResolved: literalValue };
1885
- }
1886
- return {
1887
- abbr,
1888
- defs,
1889
- mediaQuery: params.mediaQuery,
1890
- pseudoClass: params.pseudoClass,
1891
- pseudoElement: params.pseudoElement,
1892
- argResolved: literalValue
1893
- };
1955
+ return segmentWithConditionContext({ abbr, defs, argResolved: literalValue }, context);
1894
1956
  }
1895
- const base = {
1896
- abbr,
1897
- defs: {},
1898
- variableProps: props,
1899
- incremented,
1900
- variableExtraDefs: extraDefs,
1901
- argNode: argAst
1902
- };
1957
+ const base = segmentWithConditionContext(
1958
+ {
1959
+ abbr,
1960
+ defs: {},
1961
+ variableProps: props,
1962
+ incremented,
1963
+ variableExtraDefs: extraDefs,
1964
+ argNode: argAst
1965
+ },
1966
+ context
1967
+ );
1903
1968
  if (appendPx) base.appendPx = true;
1904
- if (whenPseudo) {
1905
- base.whenPseudo = whenPseudo;
1906
- } else {
1907
- base.mediaQuery = params.mediaQuery;
1908
- base.pseudoClass = params.pseudoClass;
1909
- base.pseudoElement = params.pseudoElement;
1910
- }
1911
1969
  return base;
1912
1970
  }
1913
1971
  function resolveClassNameCall(node, mediaQuery, pseudoClass, pseudoElement, whenPseudo) {
@@ -1977,34 +2035,28 @@ function resolveAddCall(node, mapping, mediaQuery, pseudoClass, pseudoElement, w
1977
2035
  const propName = propArg.value;
1978
2036
  const valueArg = node.args[1];
1979
2037
  const literalValue = tryEvaluateAddLiteral(valueArg);
1980
- if (whenPseudo) {
1981
- if (literalValue !== null) {
1982
- return { abbr: propName, defs: { [propName]: literalValue }, whenPseudo, argResolved: literalValue };
1983
- } else {
1984
- return { abbr: propName, defs: {}, whenPseudo, variableProps: [propName], incremented: false, argNode: valueArg };
1985
- }
1986
- }
2038
+ const context = {
2039
+ mediaQuery,
2040
+ pseudoClass,
2041
+ pseudoElement,
2042
+ whenPseudo
2043
+ };
1987
2044
  if (literalValue !== null) {
1988
- return {
1989
- abbr: propName,
1990
- defs: { [propName]: literalValue },
1991
- mediaQuery,
1992
- pseudoClass,
1993
- pseudoElement,
1994
- argResolved: literalValue
1995
- };
1996
- } else {
1997
- return {
2045
+ return segmentWithConditionContext(
2046
+ { abbr: propName, defs: { [propName]: literalValue }, argResolved: literalValue },
2047
+ context
2048
+ );
2049
+ }
2050
+ return segmentWithConditionContext(
2051
+ {
1998
2052
  abbr: propName,
1999
2053
  defs: {},
2000
- mediaQuery,
2001
- pseudoClass,
2002
- pseudoElement,
2003
2054
  variableProps: [propName],
2004
2055
  incremented: false,
2005
2056
  argNode: valueArg
2006
- };
2007
- }
2057
+ },
2058
+ context
2059
+ );
2008
2060
  }
2009
2061
  function tryEvaluateAddLiteral(node) {
2010
2062
  if (node.type === "StringLiteral") {
@@ -3358,11 +3410,14 @@ function toVirtualCssSpecifier(source) {
3358
3410
  import { readFileSync } from "fs";
3359
3411
  var RULE_ANNOTATION_RE = /^\/\* @truss p:([\d.]+) c:(\S+) \*\/$/;
3360
3412
  var PROPERTY_ANNOTATION_RE = /^\/\* @truss @property \*\/$/;
3413
+ var ARBITRARY_START_RE = /^\/\* @truss arbitrary:start \*\/$/;
3414
+ var ARBITRARY_END_RE = /^\/\* @truss arbitrary:end \*\/$/;
3361
3415
  var PROPERTY_VAR_RE = /^@property\s+(--\S+)/;
3362
3416
  function parseTrussCss(cssText) {
3363
3417
  const lines = cssText.split("\n");
3364
3418
  const rules = [];
3365
3419
  const properties = [];
3420
+ const arbitraryCssBlocks = [];
3366
3421
  let i = 0;
3367
3422
  while (i < lines.length) {
3368
3423
  const line = lines[i].trim();
@@ -3391,19 +3446,43 @@ function parseTrussCss(cssText) {
3391
3446
  i++;
3392
3447
  continue;
3393
3448
  }
3449
+ if (ARBITRARY_START_RE.test(line)) {
3450
+ i++;
3451
+ const blockLines = [];
3452
+ while (i < lines.length && !ARBITRARY_END_RE.test(lines[i].trim())) {
3453
+ blockLines.push(lines[i]);
3454
+ i++;
3455
+ }
3456
+ const blockText = blockLines.join("\n").trim();
3457
+ if (blockText.length > 0) {
3458
+ arbitraryCssBlocks.push({ cssText: blockText });
3459
+ }
3460
+ if (i < lines.length && ARBITRARY_END_RE.test(lines[i].trim())) {
3461
+ i++;
3462
+ }
3463
+ continue;
3464
+ }
3394
3465
  i++;
3395
3466
  }
3396
- return { rules, properties };
3467
+ return { rules, properties, arbitraryCssBlocks };
3397
3468
  }
3398
3469
  function readTrussCss(filePath) {
3399
3470
  const content = readFileSync(filePath, "utf8");
3400
3471
  return parseTrussCss(content);
3401
3472
  }
3473
+ function annotateArbitraryCssBlock(cssText) {
3474
+ const trimmed = cssText.trim();
3475
+ if (trimmed.length === 0) {
3476
+ return "";
3477
+ }
3478
+ return ["/* @truss arbitrary:start */", trimmed, "/* @truss arbitrary:end */"].join("\n");
3479
+ }
3402
3480
  function mergeTrussCss(sources) {
3403
3481
  const seenClasses = /* @__PURE__ */ new Set();
3404
3482
  const allRules = [];
3405
3483
  const seenProperties = /* @__PURE__ */ new Set();
3406
3484
  const allProperties = [];
3485
+ const allArbitraryCssBlocks = [];
3407
3486
  for (const source of sources) {
3408
3487
  for (const rule of source.rules) {
3409
3488
  if (!seenClasses.has(rule.className)) {
@@ -3417,6 +3496,7 @@ function mergeTrussCss(sources) {
3417
3496
  allProperties.push(prop);
3418
3497
  }
3419
3498
  }
3499
+ allArbitraryCssBlocks.push(...source.arbitraryCssBlocks ?? []);
3420
3500
  }
3421
3501
  allRules.sort((a, b) => {
3422
3502
  const diff = a.priority - b.priority;
@@ -3432,6 +3512,9 @@ function mergeTrussCss(sources) {
3432
3512
  lines.push(`/* @truss @property */`);
3433
3513
  lines.push(prop.cssText);
3434
3514
  }
3515
+ for (const block of allArbitraryCssBlocks) {
3516
+ lines.push(annotateArbitraryCssBlock(block.cssText));
3517
+ }
3435
3518
  return lines.join("\n");
3436
3519
  }
3437
3520
 
@@ -3440,6 +3523,7 @@ import { readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
3440
3523
  import { resolve, join } from "path";
3441
3524
  function trussEsbuildPlugin(opts) {
3442
3525
  const cssRegistry = /* @__PURE__ */ new Map();
3526
+ const arbitraryCssRegistry = /* @__PURE__ */ new Map();
3443
3527
  let mapping = null;
3444
3528
  let outDir;
3445
3529
  return {
@@ -3448,6 +3532,18 @@ function trussEsbuildPlugin(opts) {
3448
3532
  outDir = build.initialOptions.outdir ?? build.initialOptions.outdir;
3449
3533
  build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, (args) => {
3450
3534
  const code = readFileSync2(args.path, "utf8");
3535
+ if (args.path.endsWith(".css.ts")) {
3536
+ if (!mapping) {
3537
+ mapping = loadMapping(resolve(process.cwd(), opts.mapping));
3538
+ }
3539
+ const css = annotateArbitraryCssBlock(transformCssTs(code, args.path, mapping));
3540
+ if (css.length > 0) {
3541
+ arbitraryCssRegistry.set(args.path, css);
3542
+ } else {
3543
+ arbitraryCssRegistry.delete(args.path);
3544
+ }
3545
+ return { contents: code, loader: loaderForPath(args.path) };
3546
+ }
3451
3547
  if (!code.includes("Css") && !code.includes("css=")) return void 0;
3452
3548
  if (!mapping) {
3453
3549
  mapping = loadMapping(resolve(process.cwd(), opts.mapping));
@@ -3464,8 +3560,11 @@ function trussEsbuildPlugin(opts) {
3464
3560
  return { contents: result.code, loader: loaderForPath(args.path) };
3465
3561
  });
3466
3562
  build.onEnd(() => {
3467
- if (cssRegistry.size === 0) return;
3468
- const css = generateCssText(cssRegistry);
3563
+ if (cssRegistry.size === 0 && arbitraryCssRegistry.size === 0) return;
3564
+ const cssParts = [generateCssText(cssRegistry), ...arbitraryCssRegistry.values()].filter(
3565
+ (part) => part.length > 0
3566
+ );
3567
+ const css = cssParts.join("\n");
3469
3568
  const cssFileName = opts.outputCss ?? "truss.css";
3470
3569
  const cssPath = resolve(outDir ?? join(process.cwd(), "dist"), cssFileName);
3471
3570
  mkdirSync(resolve(cssPath, ".."), { recursive: true });
@@ -3565,7 +3664,7 @@ function trussPlugin(opts) {
3565
3664
  },
3566
3665
  transformIndexHtml(html) {
3567
3666
  if (isBuild) {
3568
- const stripped = html.replace(/\s*<link[^>]*href=["'][^"']*virtual:truss\.css["'][^>]*\/?>/, "");
3667
+ const stripped = html.replace(/\s*<link[^>]*href=["'][^"']*virtual:truss\.css["'][^>]*\/?>/g, "").replace(/\s*<link[^>]*href=["'][^"']*__TRUSS_CSS_HASH__["'][^>]*\/?>/g, "").replace(/\s*<link[^>]*href=["'][^"']*\/assets\/truss-[0-9a-f]+\.css["'][^>]*\/?>/g, "");
3569
3668
  const link = `<link rel="stylesheet" href="${TRUSS_CSS_PLACEHOLDER}">`;
3570
3669
  return stripped.replace("</head>", ` ${link}
3571
3670
  </head>`);