@nswds/app 1.99.0 → 1.100.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -14973,7 +14973,10 @@ function ColorCard({ name, token, hex: hex2, rgb: rgb2, hsl, oklch: oklch2, form
14973
14973
  ] }) });
14974
14974
  }
14975
14975
  var recommendedBackgroundTones = [50, 100, 200, 400, 600, 800];
14976
+ var recommendedForegroundTones = [50, 200, 400, 600, 800];
14976
14977
  var AAA_NORMAL_TEXT_RATIO = 7;
14978
+ var DEFAULT_PRIMARY_FAMILY_KEY = "blue";
14979
+ var DEFAULT_ACCENT_FAMILY_KEY = "red";
14977
14980
  var MIN_DARK_BACKGROUND_TONE_FOR_WHITE = 600;
14978
14981
  var WHITE_FOREGROUND = {
14979
14982
  token: "white",
@@ -14982,6 +14985,8 @@ var WHITE_FOREGROUND = {
14982
14985
  rgb: "rgb(255, 255, 255)",
14983
14986
  hsl: "hsl(0, 0%, 100%)",
14984
14987
  oklch: "oklch(1 0 0)",
14988
+ familyKey: "white",
14989
+ familyLabel: "White",
14985
14990
  tone: 0
14986
14991
  };
14987
14992
  function extractTone(token) {
@@ -14992,23 +14997,48 @@ function formatFamilyLabel(name, key) {
14992
14997
  if (key === "grey") return "Grey";
14993
14998
  return name.replace(/^NSW Aboriginal\s+/i, "").replace(/^NSW\s+/i, "").trim();
14994
14999
  }
14995
- function toPairingColor(color2) {
15000
+ function toPairingColor(color2, familyKey, familyLabel) {
14996
15001
  return {
14997
15002
  ...color2,
15003
+ familyKey,
15004
+ familyLabel,
14998
15005
  tone: extractTone(color2.token)
14999
15006
  };
15000
15007
  }
15001
- function getPreferredForegroundTone(backgroundTone) {
15002
- if (backgroundTone <= 200) return 800;
15003
- if (backgroundTone <= 500) return 700;
15004
- if (backgroundTone <= 650) return 250;
15005
- return 200;
15008
+ function buildPairingCollections() {
15009
+ return {
15010
+ brand: Object.entries(colors.brand).map(([key, palette]) => {
15011
+ const familyLabel = formatFamilyLabel(palette.name, key);
15012
+ return {
15013
+ key,
15014
+ label: familyLabel,
15015
+ paletteName: palette.name,
15016
+ colors: palette.colors.map((color2) => toPairingColor(color2, key, familyLabel))
15017
+ };
15018
+ }),
15019
+ aboriginal: Object.entries(colors.aboriginal).map(([key, palette]) => {
15020
+ const familyLabel = formatFamilyLabel(palette.name, key);
15021
+ return {
15022
+ key,
15023
+ label: familyLabel,
15024
+ paletteName: palette.name,
15025
+ colors: palette.colors.map((color2) => toPairingColor(color2, key, familyLabel))
15026
+ };
15027
+ })
15028
+ };
15006
15029
  }
15030
+ var pairingFamilyCollections = buildPairingCollections();
15007
15031
  function getPairRating(contrastRatio) {
15008
15032
  if (contrastRatio >= 7) return "AAA";
15009
15033
  if (contrastRatio >= 4.5) return "AA";
15010
15034
  return "AA Large";
15011
15035
  }
15036
+ function getPreferredForegroundTone(backgroundTone) {
15037
+ if (backgroundTone <= 200) return 800;
15038
+ if (backgroundTone <= 500) return 700;
15039
+ if (backgroundTone <= 650) return 250;
15040
+ return 200;
15041
+ }
15012
15042
  function isPreferredDirection(backgroundTone, foregroundTone, preferredForegroundTone) {
15013
15043
  if (preferredForegroundTone < backgroundTone) {
15014
15044
  return foregroundTone < backgroundTone;
@@ -15031,15 +15061,27 @@ function buildPair(background, foreground) {
15031
15061
  rating: getPairRating(contrastRatio)
15032
15062
  };
15033
15063
  }
15034
- function pickForeground(colorsToPair, background, minimumRatio) {
15064
+ function sortRecommendedPairs(background, pairs) {
15035
15065
  const preferredForegroundTone = getPreferredForegroundTone(background.tone);
15036
- const passingCandidates = colorsToPair.filter((color2) => color2.token !== background.token).map((color2) => buildPair(background, color2)).filter((pair) => pair.contrastRatio >= minimumRatio);
15037
- if (passingCandidates.length === 0) return null;
15038
- const preferredCandidates = passingCandidates.filter(
15039
- (pair) => isPreferredDirection(background.tone, pair.foreground.tone, preferredForegroundTone)
15040
- );
15041
- const candidates = preferredCandidates.length > 0 ? preferredCandidates : passingCandidates;
15042
- return candidates.sort((left, right) => {
15066
+ return [...pairs].sort((left, right) => {
15067
+ const leftPreferred = isPreferredDirection(
15068
+ background.tone,
15069
+ left.foreground.tone,
15070
+ preferredForegroundTone
15071
+ ) ? 0 : 1;
15072
+ const rightPreferred = isPreferredDirection(
15073
+ background.tone,
15074
+ right.foreground.tone,
15075
+ preferredForegroundTone
15076
+ ) ? 0 : 1;
15077
+ if (leftPreferred !== rightPreferred) {
15078
+ return leftPreferred - rightPreferred;
15079
+ }
15080
+ const leftWhitePenalty = left.foreground.token === WHITE_FOREGROUND.token ? 1 : 0;
15081
+ const rightWhitePenalty = right.foreground.token === WHITE_FOREGROUND.token ? 1 : 0;
15082
+ if (leftWhitePenalty !== rightWhitePenalty) {
15083
+ return leftWhitePenalty - rightWhitePenalty;
15084
+ }
15043
15085
  const leftTargetDelta = Math.abs(left.foreground.tone - preferredForegroundTone);
15044
15086
  const rightTargetDelta = Math.abs(right.foreground.tone - preferredForegroundTone);
15045
15087
  if (leftTargetDelta !== rightTargetDelta) {
@@ -15050,49 +15092,116 @@ function pickForeground(colorsToPair, background, minimumRatio) {
15050
15092
  if (leftToneGap !== rightToneGap) {
15051
15093
  return rightToneGap - leftToneGap;
15052
15094
  }
15053
- return right.contrastRatio - left.contrastRatio;
15054
- })[0];
15055
- }
15056
- function buildRecommendedPairs(colorsToPair, minimumRatio) {
15057
- const backgrounds = recommendedBackgroundTones.map((tone) => colorsToPair.find((color2) => color2.tone === tone)).filter((color2) => Boolean(color2));
15058
- const recommendedPairs = [];
15059
- for (const background of backgrounds) {
15060
- const pair = pickForeground(colorsToPair, background, minimumRatio);
15061
- const whitePair = background.tone >= MIN_DARK_BACKGROUND_TONE_FOR_WHITE ? buildPair(background, WHITE_FOREGROUND) : null;
15062
- if (pair && !recommendedPairs.some((item) => item.id === pair.id)) {
15063
- recommendedPairs.push(pair);
15095
+ if (left.foreground.familyKey === background.familyKey && right.foreground.familyKey !== background.familyKey) {
15096
+ return -1;
15064
15097
  }
15065
- if (whitePair && whitePair.contrastRatio >= minimumRatio && !recommendedPairs.some((item) => item.id === whitePair.id)) {
15066
- recommendedPairs.push(whitePair);
15098
+ if (right.foreground.familyKey === background.familyKey && left.foreground.familyKey !== background.familyKey) {
15099
+ return 1;
15067
15100
  }
15101
+ return right.contrastRatio - left.contrastRatio;
15102
+ });
15103
+ }
15104
+ function dedupeColors(colorsToDedupe) {
15105
+ return Array.from(new Map(colorsToDedupe.map((color2) => [color2.token, color2])).values());
15106
+ }
15107
+ function buildRecommendedPairsForBackground(background, foregrounds, minimumRatio) {
15108
+ const candidatePairs = foregrounds.filter((foreground) => foreground.token !== background.token).map((foreground) => buildPair(background, foreground)).filter((pair) => pair.contrastRatio >= minimumRatio);
15109
+ const whitePair = background.tone >= MIN_DARK_BACKGROUND_TONE_FOR_WHITE ? buildPair(background, WHITE_FOREGROUND) : null;
15110
+ if (whitePair && whitePair.contrastRatio >= minimumRatio) {
15111
+ candidatePairs.push(whitePair);
15068
15112
  }
15069
- return recommendedPairs;
15113
+ const dedupedPairs = Array.from(new Map(candidatePairs.map((pair) => [pair.id, pair])).values());
15114
+ return sortRecommendedPairs(background, dedupedPairs);
15115
+ }
15116
+ function findGreyFamily(families) {
15117
+ return families.find((family) => family.key.toLowerCase().includes("grey")) ?? families[0];
15118
+ }
15119
+ function getNonGreyFamilies(families) {
15120
+ const greyFamily = findGreyFamily(families);
15121
+ return families.filter((family) => family.key !== greyFamily.key);
15122
+ }
15123
+ function findFamilyByKey(families, key) {
15124
+ return key ? families.find((family) => family.key === key) : void 0;
15125
+ }
15126
+ function getDefaultPrimaryFamily(families) {
15127
+ const nonGreyFamilies = getNonGreyFamilies(families);
15128
+ return findFamilyByKey(nonGreyFamilies, DEFAULT_PRIMARY_FAMILY_KEY) ?? findFamilyByKey(nonGreyFamilies, "green") ?? nonGreyFamilies[0] ?? families[0];
15070
15129
  }
15071
- function buildRecommendedCollections(minimumRatio) {
15130
+ function getDefaultAccentFamily(families, primaryKey) {
15131
+ const nonGreyFamilies = getNonGreyFamilies(families).filter((family) => family.key !== primaryKey);
15132
+ return findFamilyByKey(nonGreyFamilies, DEFAULT_ACCENT_FAMILY_KEY) ?? nonGreyFamilies[0] ?? getDefaultPrimaryFamily(families);
15133
+ }
15134
+ function isApprovedBackgroundTone(color2) {
15135
+ return recommendedBackgroundTones.includes(
15136
+ color2.tone
15137
+ );
15138
+ }
15139
+ function isApprovedForegroundTone(color2) {
15140
+ return recommendedForegroundTones.includes(
15141
+ color2.tone
15142
+ );
15143
+ }
15144
+ function getPairingFamilies(themeCategory) {
15145
+ return pairingFamilyCollections[themeCategory];
15146
+ }
15147
+ function getDefaultAccentFamilyKey(themeCategory, primaryKey) {
15148
+ return getDefaultAccentFamily(getPairingFamilies(themeCategory), primaryKey).key;
15149
+ }
15150
+ function getPairingContext(themeCategory, primaryKey, accentKey) {
15151
+ const families = getPairingFamilies(themeCategory);
15152
+ const primary = findFamilyByKey(getNonGreyFamilies(families), primaryKey) ?? getDefaultPrimaryFamily(families);
15153
+ const accent = findFamilyByKey(
15154
+ getNonGreyFamilies(families).filter((family) => family.key !== primary.key),
15155
+ accentKey
15156
+ ) ?? getDefaultAccentFamily(families, primary.key);
15157
+ const grey = findGreyFamily(families);
15158
+ const allFamilies = [primary, accent, grey];
15159
+ const backgroundGroups = [
15160
+ {
15161
+ key: "primary",
15162
+ label: "Primary backgrounds",
15163
+ family: primary,
15164
+ backgrounds: primary.colors.filter(isApprovedBackgroundTone)
15165
+ },
15166
+ {
15167
+ key: "accent",
15168
+ label: "Accent backgrounds",
15169
+ family: accent,
15170
+ backgrounds: accent.colors.filter(isApprovedBackgroundTone)
15171
+ },
15172
+ {
15173
+ key: "grey",
15174
+ label: "Grey backgrounds",
15175
+ family: grey,
15176
+ backgrounds: grey.colors.filter(isApprovedBackgroundTone)
15177
+ }
15178
+ ];
15179
+ const backgrounds = backgroundGroups.flatMap((group) => group.backgrounds);
15180
+ const candidateForegrounds = dedupeColors(
15181
+ allFamilies.flatMap((family) => family.colors.filter(isApprovedForegroundTone))
15182
+ );
15183
+ const pairsByBackground = {};
15184
+ for (const background of backgrounds) {
15185
+ pairsByBackground[background.token] = buildRecommendedPairsForBackground(
15186
+ background,
15187
+ candidateForegrounds,
15188
+ AAA_NORMAL_TEXT_RATIO
15189
+ );
15190
+ }
15072
15191
  return {
15073
- brand: Object.entries(colors.brand).map(([key, palette]) => {
15074
- const scale2 = palette.colors.map(toPairingColor);
15075
- return {
15076
- key,
15077
- label: formatFamilyLabel(palette.name, key),
15078
- paletteName: palette.name,
15079
- colors: scale2,
15080
- recommendedPairs: buildRecommendedPairs(scale2, minimumRatio)
15081
- };
15082
- }),
15083
- aboriginal: Object.entries(colors.aboriginal).map(([key, palette]) => {
15084
- const scale2 = palette.colors.map(toPairingColor);
15085
- return {
15086
- key,
15087
- label: formatFamilyLabel(palette.name, key),
15088
- paletteName: palette.name,
15089
- colors: scale2,
15090
- recommendedPairs: buildRecommendedPairs(scale2, minimumRatio)
15091
- };
15092
- })
15192
+ accent,
15193
+ allFamilies,
15194
+ backgroundGroups,
15195
+ backgrounds,
15196
+ grey,
15197
+ pairsByBackground,
15198
+ primary,
15199
+ recommendedPairs: backgrounds.flatMap(
15200
+ (background) => pairsByBackground[background.token] ?? []
15201
+ ),
15202
+ themeCategory
15093
15203
  };
15094
15204
  }
15095
- var recommendedAaaPairingCollections = buildRecommendedCollections(AAA_NORMAL_TEXT_RATIO);
15096
15205
  function getWhiteForegroundPair(background) {
15097
15206
  return buildPair(background, WHITE_FOREGROUND);
15098
15207
  }
@@ -15244,64 +15353,86 @@ function FormatToggle({ format, setFormat }) {
15244
15353
  )
15245
15354
  ] });
15246
15355
  }
15247
- function getPreferredFamilyKey(families) {
15248
- return families.find((family) => family.key === "green")?.key ?? families[0]?.key ?? "";
15249
- }
15250
- function getDefaultPair(family) {
15251
- if (!family || family.recommendedPairs.length === 0) return null;
15252
- return family.recommendedPairs.reduce((bestPair, pair) => {
15253
- const bestDelta = Math.abs(bestPair.background.tone - 600);
15254
- const currentDelta = Math.abs(pair.background.tone - 600);
15255
- return currentDelta < bestDelta ? pair : bestPair;
15256
- }, family.recommendedPairs[0]);
15257
- }
15258
- function getFamilySwatchColor(family) {
15259
- return family.colors.find((color2) => color2.tone === 500)?.hex ?? family.colors.find((color2) => color2.tone === 400)?.hex ?? family.colors[Math.min(3, family.colors.length - 1)]?.hex ?? "transparent";
15260
- }
15356
+ var AAA_NORMAL_TEXT_THRESHOLD = "7.0:1 or higher for text below 18pt, or below 14pt bold";
15357
+ var AAA_LARGE_TEXT_THRESHOLD = "4.5:1 or higher for text at 18pt and above, or 14pt and above bold";
15358
+ var PREFERRED_BACKGROUND_TONES = [400, 600, 200, 800, 100, 50];
15261
15359
  function getToneFromToken(token) {
15262
15360
  if (!token) return null;
15263
15361
  const match = token.match(/-(\d+)$/);
15264
15362
  return match ? Number.parseInt(match[1], 10) : null;
15265
15363
  }
15266
- function isApprovedBackgroundTone(color2) {
15267
- return recommendedBackgroundTones.includes(
15268
- color2.tone
15269
- );
15364
+ function getFamilySwatchColor(family, preferredTone = 600) {
15365
+ const exactMatch = family.colors.find((color2) => color2.tone === preferredTone);
15366
+ if (exactMatch) {
15367
+ return exactMatch.hex;
15368
+ }
15369
+ const closestMatch = [...family.colors].sort(
15370
+ (left, right) => Math.abs(left.tone - preferredTone) - Math.abs(right.tone - preferredTone)
15371
+ )[0];
15372
+ return closestMatch?.hex ?? "transparent";
15270
15373
  }
15271
- function getBackgroundOptions(family) {
15272
- return family?.colors.filter(isApprovedBackgroundTone) ?? [];
15374
+ function getFamilySelectorLabel(family, themeCategory, selectionRole) {
15375
+ if (themeCategory !== "aboriginal") {
15376
+ return family.label;
15377
+ }
15378
+ const preferredTone = selectionRole === "primary colour" ? 800 : 600;
15379
+ return family.colors.find((color2) => color2.tone === preferredTone)?.name ?? family.label;
15273
15380
  }
15274
15381
  function isWhiteForegroundPair(pair) {
15275
15382
  return pair.foreground.token === "white";
15276
15383
  }
15277
- function getRecommendedPairForBackground(family, backgroundToken, preferredPairId) {
15278
- if (!family) return null;
15279
- const backgroundPairs = family.recommendedPairs.filter(
15280
- (pair) => pair.background.token === backgroundToken
15281
- );
15282
- if (backgroundPairs.length === 0) return null;
15384
+ function getPreferredPairForBackground(pairs, preferredPairId) {
15283
15385
  if (preferredPairId) {
15284
- const preferredPair = backgroundPairs.find((pair) => pair.id === preferredPairId);
15386
+ const preferredPair = pairs.find((pair) => pair.id === preferredPairId);
15285
15387
  if (preferredPair) {
15286
15388
  return preferredPair;
15287
15389
  }
15288
15390
  }
15289
- return backgroundPairs.find((pair) => !isWhiteForegroundPair(pair)) ?? backgroundPairs[0];
15391
+ return pairs.find((pair) => !isWhiteForegroundPair(pair)) ?? pairs[0] ?? null;
15290
15392
  }
15291
- function getFallbackBackgroundToken(family, preferredTone) {
15292
- const backgroundOptions = getBackgroundOptions(family);
15293
- if (preferredTone) {
15294
- const preferredBackground = backgroundOptions.find((color2) => color2.tone === preferredTone);
15295
- if (preferredBackground) {
15296
- return preferredBackground.token;
15393
+ function getDefaultBackgroundToken(context) {
15394
+ for (const tone of PREFERRED_BACKGROUND_TONES) {
15395
+ for (const group of context.backgroundGroups) {
15396
+ const match = group.backgrounds.find(
15397
+ (background) => background.tone === tone && (context.pairsByBackground[background.token]?.length ?? 0) > 0
15398
+ );
15399
+ if (match) {
15400
+ return match.token;
15401
+ }
15402
+ }
15403
+ }
15404
+ for (const tone of PREFERRED_BACKGROUND_TONES) {
15405
+ for (const group of context.backgroundGroups) {
15406
+ const match = group.backgrounds.find((background) => background.tone === tone);
15407
+ if (match) {
15408
+ return match.token;
15409
+ }
15410
+ }
15411
+ }
15412
+ return context.backgrounds[0]?.token ?? "";
15413
+ }
15414
+ function resolveBackgroundToken(context, preferredToken, preferredTone) {
15415
+ if (preferredToken && context.backgrounds.some((background) => background.token === preferredToken)) {
15416
+ return preferredToken;
15417
+ }
15418
+ if (preferredTone !== null && preferredTone !== void 0) {
15419
+ for (const group of context.backgroundGroups) {
15420
+ const match = group.backgrounds.find((background) => background.tone === preferredTone);
15421
+ if (match) {
15422
+ return match.token;
15423
+ }
15297
15424
  }
15298
15425
  }
15299
- return getDefaultPair(family)?.background.token ?? backgroundOptions[0]?.token ?? "";
15426
+ return getDefaultBackgroundToken(context);
15300
15427
  }
15301
15428
  function ColorPairingToolLoading() {
15302
15429
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "animate-pulse space-y-6", children: [
15303
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-40 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15304
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(20rem,0.9fr)]", children: [
15430
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-32 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15431
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6 lg:grid-cols-2", children: [
15432
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-48 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15433
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-48 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" })
15434
+ ] }),
15435
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(18rem,0.72fr)]", children: [
15305
15436
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-96 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15306
15437
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-96 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" })
15307
15438
  ] })
@@ -15309,24 +15440,32 @@ function ColorPairingToolLoading() {
15309
15440
  }
15310
15441
  function getInitialPairingState(searchParams) {
15311
15442
  const paletteParam = searchParams.get("palette");
15312
- const familyParam = searchParams.get("family");
15443
+ const primaryParam = searchParams.get("primary");
15444
+ const accentParam = searchParams.get("accent");
15313
15445
  const pairParam = searchParams.get("pair");
15314
15446
  const backgroundParam = searchParams.get("background");
15315
15447
  const themeCategory = paletteParam === "brand" || paletteParam === "aboriginal" ? paletteParam : "brand";
15316
- const families = recommendedAaaPairingCollections[themeCategory];
15317
- const familyKey = families.some((family) => family.key === familyParam) ? familyParam : getPreferredFamilyKey(families);
15318
- const activeFamily = families.find((family) => family.key === familyKey) ?? families[0];
15319
- const backgroundOptions = getBackgroundOptions(activeFamily);
15320
- const pairBackgroundToken = activeFamily?.recommendedPairs.find((pair) => pair.id === pairParam)?.background.token ?? null;
15321
- const selectedBackgroundToken = backgroundOptions.some((color2) => color2.token === backgroundParam) ? backgroundParam : getFallbackBackgroundToken(
15322
- activeFamily,
15448
+ const context = getPairingContext(themeCategory, primaryParam, accentParam);
15449
+ const pairBackgroundToken = context.recommendedPairs.find((pair) => pair.id === pairParam)?.background.token ?? null;
15450
+ const selectedBackgroundToken = resolveBackgroundToken(
15451
+ context,
15452
+ backgroundParam ?? pairBackgroundToken,
15323
15453
  getToneFromToken(backgroundParam ?? pairBackgroundToken)
15324
15454
  );
15325
- const selectedPairId = getRecommendedPairForBackground(activeFamily, selectedBackgroundToken, pairParam)?.id ?? "";
15326
- return { familyKey, selectedBackgroundToken, selectedPairId, themeCategory };
15455
+ const selectedPairId = getPreferredPairForBackground(
15456
+ context.pairsByBackground[selectedBackgroundToken] ?? [],
15457
+ pairParam
15458
+ )?.id ?? "";
15459
+ return {
15460
+ accentKey: context.accent.key,
15461
+ primaryKey: context.primary.key,
15462
+ selectedBackgroundToken,
15463
+ selectedPairId,
15464
+ themeCategory
15465
+ };
15327
15466
  }
15328
15467
  function PairPreview({
15329
- family,
15468
+ familySummary,
15330
15469
  isRecommended,
15331
15470
  pair
15332
15471
  }) {
@@ -15354,7 +15493,7 @@ function PairPreview({
15354
15493
  },
15355
15494
  children: [
15356
15495
  /* @__PURE__ */ jsxRuntime.jsx(Icons.palette, { "data-slot": "icon", className: "size-4" }),
15357
- family.label
15496
+ familySummary
15358
15497
  ]
15359
15498
  }
15360
15499
  ),
@@ -15375,10 +15514,10 @@ function PairPreview({
15375
15514
  )
15376
15515
  ] }),
15377
15516
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-xl space-y-4 pb-12 sm:pb-16", children: [
15378
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold tracking-[0.22em] uppercase", children: whiteForeground && !isRecommended ? "White on colour example" : whiteForeground ? "White on colour" : "Tone on tone" }),
15517
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold tracking-[0.22em] uppercase", children: whiteForeground && !isRecommended ? "White on colour example" : whiteForeground ? "White on colour" : "Colour on colour" }),
15379
15518
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
15380
15519
  /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "max-w-lg text-4xl leading-none font-bold text-current sm:text-5xl", children: "Pair colour with confidence." }),
15381
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-md text-sm/6 sm:text-base/7", children: whiteForeground ? isRecommended ? "Use white text on dark colour only when it meets AAA for headings, body copy, and calls to action." : "This white text example is included for reference on approved dark backgrounds. Check the result card below before using it for normal or large text." : "Use only AAA-recommended tone-on-tone combinations for headings, body copy, and calls to action on colour." })
15520
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-md text-sm/6 sm:text-base/7", children: whiteForeground ? isRecommended ? "Use white text on dark colour only when it meets AAA for headings, body copy, and calls to action." : "This white text example is included for reference on approved dark backgrounds. Check the result card below before using it for normal or large text." : "Use only AAA-recommended combinations across your selected primary, accent, and grey families." })
15382
15521
  ] })
15383
15522
  ] }),
15384
15523
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
@@ -15415,6 +15554,41 @@ function PairPreview({
15415
15554
  }
15416
15555
  ) });
15417
15556
  }
15557
+ function PreviewFallbackCard({
15558
+ familySummary,
15559
+ selectedBackground
15560
+ }) {
15561
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className: "gap-0 overflow-hidden py-0", children: /* @__PURE__ */ jsxRuntime.jsx(
15562
+ "div",
15563
+ {
15564
+ className: "p-6 sm:min-h-[26rem] sm:p-8",
15565
+ style: {
15566
+ backgroundColor: selectedBackground.hex,
15567
+ color: selectedBackground.tone >= 600 ? "#ffffff" : "#002664"
15568
+ },
15569
+ children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-[22rem] flex-col justify-between gap-8", children: [
15570
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2 rounded-full border border-current bg-transparent px-3 py-1 text-[0.72rem] font-semibold tracking-[0.16em] uppercase", children: [
15571
+ /* @__PURE__ */ jsxRuntime.jsx(Icons.palette, { "data-slot": "icon", className: "size-4" }),
15572
+ familySummary
15573
+ ] }) }),
15574
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-xl space-y-4 pb-12 sm:pb-16", children: [
15575
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold tracking-[0.22em] uppercase", children: "Approved background" }),
15576
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
15577
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "max-w-lg text-4xl leading-none font-bold text-current sm:text-5xl", children: "Pair colour with confidence." }),
15578
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-md text-sm/6 sm:text-base/7", children: "This approved background tone does not currently have a recommended AAA foreground in this tool." })
15579
+ ] })
15580
+ ] }),
15581
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
15582
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2 rounded-full bg-current/10 px-4 py-2 text-sm font-semibold", children: [
15583
+ "No recommendation",
15584
+ /* @__PURE__ */ jsxRuntime.jsx(Icons.info, { "data-slot": "icon", className: "size-4" })
15585
+ ] }),
15586
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex rounded-full border border-current px-4 py-2 text-sm", children: selectedBackground.token })
15587
+ ] })
15588
+ ] })
15589
+ }
15590
+ ) });
15591
+ }
15418
15592
  function PairDetailCard({
15419
15593
  color: color2,
15420
15594
  format,
@@ -15530,7 +15704,7 @@ function PairComplianceCard({ pair }) {
15530
15704
  {
15531
15705
  label: "AAA normal text",
15532
15706
  passes: pair.passes.aaaText,
15533
- threshold: "7.0:1 or higher"
15707
+ threshold: AAA_NORMAL_TEXT_THRESHOLD
15534
15708
  }
15535
15709
  ),
15536
15710
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -15538,7 +15712,7 @@ function PairComplianceCard({ pair }) {
15538
15712
  {
15539
15713
  label: "AAA large text",
15540
15714
  passes: pair.passes.aaaLarge,
15541
- threshold: "4.5:1 or higher"
15715
+ threshold: AAA_LARGE_TEXT_THRESHOLD
15542
15716
  }
15543
15717
  )
15544
15718
  ] })
@@ -15576,7 +15750,8 @@ function WhiteTextExampleCard({ pair }) {
15576
15750
  "AAA large text: ",
15577
15751
  /* @__PURE__ */ jsxRuntime.jsx("strong", { children: pair.passes.aaaLarge ? "pass" : "fail" })
15578
15752
  ] })
15579
- ] })
15753
+ ] }),
15754
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm/6", children: "Normal text means body text and smaller headings below 18pt, or below 14pt when bold. Large text means 18pt and above, or 14pt and above when bold." })
15580
15755
  ] })
15581
15756
  }
15582
15757
  ),
@@ -15593,7 +15768,7 @@ function WhiteTextExampleCard({ pair }) {
15593
15768
  {
15594
15769
  label: "AAA normal text",
15595
15770
  passes: pair.passes.aaaText,
15596
- threshold: "7.0:1 or higher"
15771
+ threshold: AAA_NORMAL_TEXT_THRESHOLD
15597
15772
  }
15598
15773
  ),
15599
15774
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -15601,42 +15776,90 @@ function WhiteTextExampleCard({ pair }) {
15601
15776
  {
15602
15777
  label: "AAA large text",
15603
15778
  passes: pair.passes.aaaLarge,
15604
- threshold: "4.5:1 or higher"
15779
+ threshold: AAA_LARGE_TEXT_THRESHOLD
15605
15780
  }
15606
15781
  )
15607
15782
  ] })
15608
15783
  ] })
15609
15784
  ] });
15610
15785
  }
15786
+ function ColorFamilySelector({
15787
+ families,
15788
+ label,
15789
+ selectedKey,
15790
+ selectionRole,
15791
+ themeCategory,
15792
+ onSelect
15793
+ }) {
15794
+ const swatchTone = selectionRole === "primary colour" ? 800 : 600;
15795
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
15796
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-medium", children: label }),
15797
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2 lg:grid-cols-4 xl:grid-cols-5", children: families.map((family) => {
15798
+ const isSelected = family.key === selectedKey;
15799
+ const familySelectorLabel = getFamilySelectorLabel(family, themeCategory, selectionRole);
15800
+ return /* @__PURE__ */ jsxRuntime.jsxs(
15801
+ "button",
15802
+ {
15803
+ type: "button",
15804
+ "aria-label": `Select ${familySelectorLabel} as ${selectionRole}`,
15805
+ "aria-pressed": isSelected,
15806
+ onClick: () => onSelect(family.key),
15807
+ className: cn(
15808
+ "group relative flex w-full items-center gap-3 rounded-sm border px-3 py-2 text-left transition-colors",
15809
+ "border-grey-200 bg-background hover:bg-primary-800/10 hover:text-foreground dark:hover:bg-white/10",
15810
+ isSelected && "border-grey-800 dark:border-grey-400"
15811
+ ),
15812
+ children: [
15813
+ /* @__PURE__ */ jsxRuntime.jsx(
15814
+ "span",
15815
+ {
15816
+ className: "size-4 shrink-0 rounded-full",
15817
+ style: { backgroundColor: getFamilySwatchColor(family, swatchTone) }
15818
+ }
15819
+ ),
15820
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium", children: familySelectorLabel })
15821
+ ]
15822
+ },
15823
+ family.key
15824
+ );
15825
+ }) })
15826
+ ] });
15827
+ }
15611
15828
  function ColorPairingToolContent() {
15612
15829
  const searchParams = navigation.useSearchParams();
15613
15830
  const {
15614
- familyKey: initialFamilyKey,
15831
+ accentKey: initialAccentKey,
15832
+ primaryKey: initialPrimaryKey,
15615
15833
  selectedBackgroundToken: initialSelectedBackgroundToken,
15616
15834
  selectedPairId: initialSelectedPairId,
15617
15835
  themeCategory: initialThemeCategory
15618
15836
  } = getInitialPairingState(searchParams);
15619
15837
  const [themeCategory, setThemeCategory] = React5.useState(initialThemeCategory);
15620
15838
  const [format, setFormat] = React5.useState("hex");
15621
- const [activeFamilyKey, setActiveFamilyKey] = React5.useState(initialFamilyKey);
15839
+ const [primaryFamilyKey, setPrimaryFamilyKey] = React5.useState(initialPrimaryKey);
15840
+ const [accentFamilyKey, setAccentFamilyKey] = React5.useState(initialAccentKey);
15622
15841
  const [selectedBackgroundToken, setSelectedBackgroundToken] = React5.useState(
15623
15842
  initialSelectedBackgroundToken
15624
15843
  );
15625
15844
  const [selectedPairId, setSelectedPairId] = React5.useState(initialSelectedPairId);
15626
- const families = recommendedAaaPairingCollections[themeCategory];
15627
- const resolvedActiveFamilyKey = families.some((family) => family.key === activeFamilyKey) ? activeFamilyKey : getPreferredFamilyKey(families);
15628
- const activeFamily = families.find((family) => family.key === resolvedActiveFamilyKey) ?? families[0];
15629
- const backgroundOptions = getBackgroundOptions(activeFamily);
15630
- const selectedBackground = backgroundOptions.find((color2) => color2.token === selectedBackgroundToken) ?? backgroundOptions[0] ?? activeFamily?.colors[0] ?? null;
15631
- const selectedPair = selectedBackground ? getRecommendedPairForBackground(activeFamily, selectedBackground.token, selectedPairId) : null;
15845
+ const themeFamilies = getPairingFamilies(themeCategory);
15846
+ const context = getPairingContext(themeCategory, primaryFamilyKey, accentFamilyKey);
15847
+ const selectableFamilies = themeFamilies.filter((family) => family.key !== context.grey.key);
15848
+ const selectedBackground = context.backgrounds.find((background) => background.token === selectedBackgroundToken) ?? context.backgrounds[0] ?? null;
15849
+ const selectedBackgroundPairs = selectedBackground ? context.pairsByBackground[selectedBackground.token] ?? [] : [];
15850
+ const selectedPair = getPreferredPairForBackground(selectedBackgroundPairs, selectedPairId);
15632
15851
  const whiteForegroundExample = selectedBackground && supportsWhiteForegroundPreview(selectedBackground) ? getWhiteForegroundPair(selectedBackground) : null;
15633
- const previewPair = selectedPair ?? whiteForegroundExample;
15852
+ const previewPair = selectedPair ?? whiteForegroundExample ?? null;
15634
15853
  const showWhiteForegroundExample = Boolean(whiteForegroundExample) && (!selectedPair || selectedPair.foreground.token !== "white");
15635
15854
  const detailForeground = selectedPair?.foreground ?? whiteForegroundExample?.foreground ?? null;
15636
- const updateUrlParams = (nextThemeCategory, nextFamilyKey, nextSelectedBackgroundToken, nextSelectedPairId) => {
15855
+ const familySummary = [context.primary.label, context.accent.label, context.grey.label].join(
15856
+ " + "
15857
+ );
15858
+ const updateUrlParams = (nextThemeCategory, nextPrimaryKey, nextAccentKey, nextSelectedBackgroundToken, nextSelectedPairId) => {
15637
15859
  const params = new URLSearchParams(window.location.search);
15638
15860
  params.set("palette", nextThemeCategory);
15639
- params.set("family", nextFamilyKey);
15861
+ params.set("primary", nextPrimaryKey);
15862
+ params.set("accent", nextAccentKey);
15640
15863
  params.set("background", nextSelectedBackgroundToken);
15641
15864
  if (nextSelectedPairId) {
15642
15865
  params.set("pair", nextSelectedPairId);
@@ -15649,79 +15872,87 @@ function ColorPairingToolContent() {
15649
15872
  `${window.location.pathname}?${params.toString()}${window.location.hash}`
15650
15873
  );
15651
15874
  };
15652
- const handleThemeCategoryChange = (nextThemeCategory) => {
15653
- const nextFamilies = recommendedAaaPairingCollections[nextThemeCategory];
15654
- const nextFamilyKey = nextFamilies.some((family) => family.key === activeFamilyKey) ? activeFamilyKey : getPreferredFamilyKey(nextFamilies);
15655
- const nextActiveFamily = nextFamilies.find((family) => family.key === nextFamilyKey) ?? nextFamilies[0];
15656
- const nextSelectedBackgroundToken = getFallbackBackgroundToken(
15657
- nextActiveFamily,
15658
- getToneFromToken(selectedBackgroundToken)
15659
- );
15660
- const nextSelectedPair = getRecommendedPairForBackground(
15661
- nextActiveFamily,
15662
- nextSelectedBackgroundToken,
15663
- selectedPairId
15875
+ const syncSelection = (nextThemeCategory, nextPrimaryKey, nextAccentKey, preferredBackgroundToken, preferredPairId) => {
15876
+ const nextContext = getPairingContext(nextThemeCategory, nextPrimaryKey, nextAccentKey);
15877
+ const nextSelectedBackgroundToken = resolveBackgroundToken(
15878
+ nextContext,
15879
+ preferredBackgroundToken,
15880
+ getToneFromToken(preferredBackgroundToken)
15664
15881
  );
15882
+ const nextSelectedPairId = getPreferredPairForBackground(
15883
+ nextContext.pairsByBackground[nextSelectedBackgroundToken] ?? [],
15884
+ preferredPairId
15885
+ )?.id ?? "";
15665
15886
  setThemeCategory(nextThemeCategory);
15666
- setActiveFamilyKey(nextFamilyKey);
15887
+ setPrimaryFamilyKey(nextContext.primary.key);
15888
+ setAccentFamilyKey(nextContext.accent.key);
15667
15889
  setSelectedBackgroundToken(nextSelectedBackgroundToken);
15668
- setSelectedPairId(nextSelectedPair?.id ?? "");
15890
+ setSelectedPairId(nextSelectedPairId);
15669
15891
  updateUrlParams(
15670
15892
  nextThemeCategory,
15671
- nextFamilyKey,
15893
+ nextContext.primary.key,
15894
+ nextContext.accent.key,
15672
15895
  nextSelectedBackgroundToken,
15673
- nextSelectedPair?.id ?? ""
15896
+ nextSelectedPairId
15674
15897
  );
15675
15898
  };
15676
- const handleFamilyChange = (nextFamilyKey) => {
15677
- const nextActiveFamily = families.find((family) => family.key === nextFamilyKey);
15678
- const nextSelectedBackgroundToken = getFallbackBackgroundToken(
15679
- nextActiveFamily,
15680
- getToneFromToken(selectedBackgroundToken)
15899
+ const handleThemeCategoryChange = (nextThemeCategory) => {
15900
+ syncSelection(
15901
+ nextThemeCategory,
15902
+ primaryFamilyKey,
15903
+ accentFamilyKey,
15904
+ selectedBackgroundToken,
15905
+ selectedPairId
15681
15906
  );
15682
- const nextSelectedPair = getRecommendedPairForBackground(
15683
- nextActiveFamily,
15684
- nextSelectedBackgroundToken,
15907
+ };
15908
+ const handlePrimaryColorChange = (nextPrimaryKey) => {
15909
+ const nextAccentKey = nextPrimaryKey === accentFamilyKey ? getDefaultAccentFamilyKey(themeCategory, nextPrimaryKey) : accentFamilyKey;
15910
+ syncSelection(
15911
+ themeCategory,
15912
+ nextPrimaryKey,
15913
+ nextAccentKey,
15914
+ selectedBackgroundToken,
15685
15915
  selectedPairId
15686
15916
  );
15687
- setActiveFamilyKey(nextFamilyKey);
15688
- setSelectedBackgroundToken(nextSelectedBackgroundToken);
15689
- setSelectedPairId(nextSelectedPair?.id ?? "");
15690
- updateUrlParams(
15917
+ };
15918
+ const handleAccentColorChange = (nextAccentKey) => {
15919
+ if (nextAccentKey === primaryFamilyKey) return;
15920
+ syncSelection(
15691
15921
  themeCategory,
15692
- nextFamilyKey,
15693
- nextSelectedBackgroundToken,
15694
- nextSelectedPair?.id ?? ""
15922
+ primaryFamilyKey,
15923
+ nextAccentKey,
15924
+ selectedBackgroundToken,
15925
+ selectedPairId
15695
15926
  );
15696
15927
  };
15697
15928
  const handleBackgroundChange = (nextSelectedBackgroundToken) => {
15698
- const nextSelectedPair = getRecommendedPairForBackground(
15699
- activeFamily,
15700
- nextSelectedBackgroundToken,
15929
+ const nextSelectedPairId = getPreferredPairForBackground(
15930
+ context.pairsByBackground[nextSelectedBackgroundToken] ?? [],
15701
15931
  selectedPairId
15702
- );
15932
+ )?.id ?? "";
15703
15933
  setSelectedBackgroundToken(nextSelectedBackgroundToken);
15704
- setSelectedPairId(nextSelectedPair?.id ?? "");
15934
+ setSelectedPairId(nextSelectedPairId);
15705
15935
  updateUrlParams(
15706
15936
  themeCategory,
15707
- resolvedActiveFamilyKey,
15937
+ context.primary.key,
15938
+ context.accent.key,
15708
15939
  nextSelectedBackgroundToken,
15709
- nextSelectedPair?.id ?? ""
15940
+ nextSelectedPairId
15710
15941
  );
15711
15942
  };
15712
15943
  const handlePairChange = (nextSelectedPairId) => {
15713
- const nextSelectedPair = activeFamily?.recommendedPairs.find((pair) => pair.id === nextSelectedPairId) ?? null;
15714
- if (!nextSelectedPair) return;
15715
- setSelectedBackgroundToken(nextSelectedPair.background.token);
15944
+ const nextSelectedPair = selectedBackgroundPairs.find((pair) => pair.id === nextSelectedPairId);
15945
+ if (!nextSelectedPair || !selectedBackground) return;
15716
15946
  setSelectedPairId(nextSelectedPairId);
15717
15947
  updateUrlParams(
15718
15948
  themeCategory,
15719
- resolvedActiveFamilyKey,
15720
- nextSelectedPair.background.token,
15949
+ context.primary.key,
15950
+ context.accent.key,
15951
+ selectedBackground.token,
15721
15952
  nextSelectedPairId
15722
15953
  );
15723
15954
  };
15724
- if (!activeFamily || !selectedBackground) {
15955
+ if (!selectedBackground) {
15725
15956
  return /* @__PURE__ */ jsxRuntime.jsx(Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(CardHeader, { children: [
15726
15957
  /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { children: "No approved background tones available" }),
15727
15958
  /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { children: "No approved tones are available for the current palette." })
@@ -15745,194 +15976,207 @@ function ColorPairingToolContent() {
15745
15976
  ] }),
15746
15977
  /* @__PURE__ */ jsxRuntime.jsx(FormatToggle, { format, setFormat })
15747
15978
  ] }),
15748
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-3", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-2 gap-2 lg:grid-cols-4 xl:grid-cols-5", children: families.map((family) => {
15749
- const isActive = family.key === resolvedActiveFamilyKey;
15750
- return /* @__PURE__ */ jsxRuntime.jsxs(
15751
- "button",
15979
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-6", children: [
15980
+ /* @__PURE__ */ jsxRuntime.jsx(
15981
+ ColorFamilySelector,
15982
+ {
15983
+ label: "Primary colour",
15984
+ families: selectableFamilies,
15985
+ selectedKey: context.primary.key,
15986
+ selectionRole: "primary colour",
15987
+ themeCategory,
15988
+ onSelect: handlePrimaryColorChange
15989
+ }
15990
+ ),
15991
+ /* @__PURE__ */ jsxRuntime.jsx(
15992
+ ColorFamilySelector,
15993
+ {
15994
+ label: "Accent colour",
15995
+ families: selectableFamilies.filter((family) => family.key !== context.primary.key),
15996
+ selectedKey: context.accent.key,
15997
+ selectionRole: "accent colour",
15998
+ themeCategory,
15999
+ onSelect: handleAccentColorChange
16000
+ }
16001
+ )
16002
+ ] }),
16003
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
16004
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Included families:" }),
16005
+ context.allFamilies.map((family) => /* @__PURE__ */ jsxRuntime.jsxs(
16006
+ "span",
15752
16007
  {
15753
- type: "button",
15754
- onClick: () => handleFamilyChange(family.key),
15755
- className: cn(
15756
- "group relative flex w-full items-center gap-3 rounded-sm border px-3 py-2 text-left transition-colors",
15757
- "border-grey-200 bg-background hover:bg-primary-800/10 hover:text-foreground dark:hover:bg-white/10",
15758
- isActive && "border-grey-800 dark:border-grey-400"
15759
- ),
16008
+ className: "inline-flex items-center gap-2 rounded-full border border-grey-200 px-3 py-1 text-muted-foreground dark:border-grey-700",
15760
16009
  children: [
15761
16010
  /* @__PURE__ */ jsxRuntime.jsx(
15762
16011
  "span",
15763
16012
  {
15764
- className: "size-4 shrink-0 rounded-full",
16013
+ className: "size-2.5 rounded-full",
15765
16014
  style: { backgroundColor: getFamilySwatchColor(family) }
15766
16015
  }
15767
16016
  ),
15768
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium", children: family.label })
16017
+ family.label
15769
16018
  ]
15770
16019
  },
15771
16020
  family.key
15772
- );
15773
- }) }) }),
15774
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
15775
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid items-start gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(17rem,0.68fr)]", children: [
15776
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
15777
- previewPair ? /* @__PURE__ */ jsxRuntime.jsx(
15778
- PairPreview,
15779
- {
15780
- family: activeFamily,
15781
- isRecommended: Boolean(selectedPair),
15782
- pair: previewPair
15783
- }
15784
- ) : /* @__PURE__ */ jsxRuntime.jsx(Card, { className: "gap-0 overflow-hidden py-0", children: /* @__PURE__ */ jsxRuntime.jsx(
15785
- "div",
15786
- {
15787
- className: "p-6 sm:min-h-[26rem] sm:p-8",
15788
- style: {
15789
- backgroundColor: selectedBackground.hex,
15790
- color: selectedBackground.tone >= 600 ? "#ffffff" : "#002664"
15791
- },
15792
- children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex min-h-[22rem] flex-col justify-between gap-8", children: [
15793
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2 rounded-full border border-current bg-transparent px-3 py-1 text-[0.72rem] font-semibold tracking-[0.16em] uppercase", children: [
15794
- /* @__PURE__ */ jsxRuntime.jsx(Icons.palette, { "data-slot": "icon", className: "size-4" }),
15795
- activeFamily.label
15796
- ] }) }),
15797
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-xl space-y-4 pb-12 sm:pb-16", children: [
15798
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold tracking-[0.22em] uppercase", children: "Approved background" }),
15799
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
15800
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "max-w-lg text-4xl leading-none font-bold text-current sm:text-5xl", children: "Pair colour with confidence." }),
15801
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "max-w-md text-sm/6 sm:text-base/7", children: "This approved background tone does not currently have a recommended AAA foreground in this tool." })
15802
- ] })
15803
- ] }),
15804
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
15805
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-2 rounded-full bg-current/10 px-4 py-2 text-sm font-semibold", children: [
15806
- "No recommendation",
15807
- /* @__PURE__ */ jsxRuntime.jsx(Icons.info, { "data-slot": "icon", className: "size-4" })
15808
- ] }),
15809
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex rounded-full border border-current px-4 py-2 text-sm", children: selectedBackground.token })
15810
- ] })
15811
- ] })
15812
- }
15813
- ) }),
15814
- /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
15815
- /* @__PURE__ */ jsxRuntime.jsxs(CardHeader, { className: "gap-2 border-b", children: [
15816
- /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { className: "text-base", children: "Background tone strip" }),
15817
- /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { children: "Only approved background tones are shown. Filled chips have a recommended AAA foreground pair, and dark backgrounds may also offer white text where it passes AAA." })
15818
- ] }),
15819
- /* @__PURE__ */ jsxRuntime.jsxs(CardContent, { className: "space-y-4", children: [
15820
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-5 gap-2 sm:grid-cols-7 lg:grid-cols-10 xl:grid-cols-[repeat(19,minmax(0,1fr))]", children: backgroundOptions.map((color2) => {
15821
- const pair = getRecommendedPairForBackground(activeFamily, color2.token);
15822
- const hasWhiteExample = supportsWhiteForegroundPreview(color2);
15823
- const isSelected = selectedBackground.token === color2.token;
15824
- return /* @__PURE__ */ jsxRuntime.jsxs(
16021
+ ))
16022
+ ] }),
16023
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid items-start gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(18rem,0.72fr)]", children: [
16024
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
16025
+ previewPair ? /* @__PURE__ */ jsxRuntime.jsx(
16026
+ PairPreview,
16027
+ {
16028
+ familySummary,
16029
+ isRecommended: Boolean(selectedPair),
16030
+ pair: previewPair
16031
+ }
16032
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
16033
+ PreviewFallbackCard,
16034
+ {
16035
+ familySummary,
16036
+ selectedBackground
16037
+ }
16038
+ ),
16039
+ /* @__PURE__ */ jsxRuntime.jsxs(Card, { children: [
16040
+ /* @__PURE__ */ jsxRuntime.jsxs(CardHeader, { className: "gap-2 border-b", children: [
16041
+ /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { className: "text-base", children: "Approved backgrounds" }),
16042
+ /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { children: "Choose from your selected primary, accent, and grey background tones. Filled chips have at least one recommended AAA foreground. Foregrounds are limited to white and tones 50, 200, 400, 600, and 800." })
16043
+ ] }),
16044
+ /* @__PURE__ */ jsxRuntime.jsxs(CardContent, { className: "space-y-5", children: [
16045
+ context.backgroundGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
16046
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
16047
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: group.label }),
16048
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: group.family.label })
16049
+ ] }),
16050
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 gap-2 sm:grid-cols-6", children: group.backgrounds.map((background) => {
16051
+ const pairs = context.pairsByBackground[background.token] ?? [];
16052
+ const preferredPair = selectedBackground.token === background.token ? selectedPair ?? getPreferredPairForBackground(pairs) : getPreferredPairForBackground(pairs);
16053
+ const hasWhiteExample = supportsWhiteForegroundPreview(background);
16054
+ const isSelected = selectedBackground.token === background.token;
16055
+ const ariaLabel = pairs.length > 0 ? `Select ${background.token} background, recommended with ${preferredPair?.foreground.token ?? "an AAA foreground"}` : hasWhiteExample ? `Select ${background.token} background, white text example available` : `Select ${background.token} background, no recommended foreground in this tool`;
16056
+ return /* @__PURE__ */ jsxRuntime.jsx(
15825
16057
  "button",
15826
16058
  {
15827
16059
  type: "button",
15828
- onClick: () => handleBackgroundChange(color2.token),
16060
+ "aria-label": ariaLabel,
16061
+ "aria-pressed": isSelected,
16062
+ onClick: () => handleBackgroundChange(background.token),
15829
16063
  className: cn(
15830
16064
  "group relative h-12 rounded-sm border border-grey-200 transition-transform hover:scale-[1.03] dark:border-grey-700",
15831
16065
  isSelected && "ring-2 ring-primary-500 ring-offset-2 ring-offset-background"
15832
16066
  ),
15833
- style: { backgroundColor: color2.hex },
15834
- children: [
15835
- pair ? /* @__PURE__ */ jsxRuntime.jsx(
15836
- "span",
15837
- {
15838
- className: "absolute top-1/2 left-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/30 shadow-sm",
15839
- style: { backgroundColor: pair.foreground.hex }
15840
- }
15841
- ) : null,
15842
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "sr-only", children: [
15843
- color2.token,
15844
- pair ? ` paired with ${pair.foreground.token}` : hasWhiteExample ? " has a white text example in this tool" : " has no recommended pair in this tool"
15845
- ] })
15846
- ]
16067
+ style: { backgroundColor: background.hex },
16068
+ children: preferredPair ? /* @__PURE__ */ jsxRuntime.jsx(
16069
+ "span",
16070
+ {
16071
+ className: "absolute top-1/2 left-1/2 size-3 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/30 shadow-sm",
16072
+ style: { backgroundColor: preferredPair.foreground.hex }
16073
+ }
16074
+ ) : null
15847
16075
  },
15848
- color2.token
16076
+ background.token
15849
16077
  );
15850
- }) }),
15851
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-4 text-sm text-muted-foreground", children: [
15852
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
15853
- "Selected background:",
15854
- " ",
15855
- /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-foreground", children: selectedBackground.token })
15856
- ] }),
15857
- /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
15858
- selectedPair ? "Recommended foreground:" : "White text example:",
15859
- " ",
15860
- /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-foreground", children: selectedPair?.foreground.token ?? whiteForegroundExample?.foreground.token ?? "None" })
15861
- ] })
16078
+ }) })
16079
+ ] }, group.key)),
16080
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-4 text-sm text-muted-foreground", children: [
16081
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
16082
+ "Selected background:",
16083
+ " ",
16084
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-foreground", children: selectedBackground.token })
16085
+ ] }),
16086
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
16087
+ selectedPair ? "Selected foreground:" : "White text example:",
16088
+ " ",
16089
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { className: "text-foreground", children: selectedPair?.foreground.token ?? whiteForegroundExample?.foreground.token ?? "None" })
15862
16090
  ] })
15863
16091
  ] })
15864
- ] }),
15865
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
15866
- /* @__PURE__ */ jsxRuntime.jsx(PairDetailCard, { color: selectedBackground, format, role: "Background" }),
15867
- detailForeground ? /* @__PURE__ */ jsxRuntime.jsx(PairDetailCard, { color: detailForeground, format, role: "Foreground" }) : /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "gap-4", children: [
15868
- /* @__PURE__ */ jsxRuntime.jsx(CardHeader, { className: "gap-3 border-b", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
15869
- /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { children: "Foreground" }),
15870
- /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { children: "No recommended foreground available" })
15871
- ] }) }),
15872
- /* @__PURE__ */ jsxRuntime.jsx(CardContent, { children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "Select another approved tone or review the recommended pairs list for the same family." }) })
15873
- ] })
15874
16092
  ] })
15875
16093
  ] }),
15876
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "h-fit", children: [
15877
- /* @__PURE__ */ jsxRuntime.jsxs(CardHeader, { className: "gap-2 border-b", children: [
15878
- /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { className: "text-base", children: "Recommended pairs" }),
15879
- /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { children: "Recommended AAA combinations for this family, including white on eligible dark backgrounds." })
15880
- ] }),
15881
- /* @__PURE__ */ jsxRuntime.jsx(CardContent, { className: "grid gap-3", children: activeFamily.recommendedPairs.map((pair) => {
15882
- const isActive = selectedPair?.id === pair.id;
15883
- return /* @__PURE__ */ jsxRuntime.jsxs(
15884
- "button",
15885
- {
15886
- type: "button",
15887
- onClick: () => handlePairChange(pair.id),
15888
- className: cn(
15889
- "rounded-sm border p-4 text-left transition-colors",
15890
- isActive ? "border-primary-500 bg-primary-50/70 dark:bg-primary-950/30" : "border-grey-200 hover:border-grey-400 hover:bg-grey-50 dark:border-grey-700 dark:hover:border-grey-500 dark:hover:bg-grey-900/70"
15891
- ),
15892
- children: [
15893
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4", children: [
15894
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
15895
- /* @__PURE__ */ jsxRuntime.jsx(
15896
- "div",
15897
- {
15898
- className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
15899
- style: { backgroundColor: pair.background.hex }
15900
- }
15901
- ),
15902
- /* @__PURE__ */ jsxRuntime.jsx(
15903
- "div",
15904
- {
15905
- className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
15906
- style: { backgroundColor: pair.foreground.hex }
15907
- }
15908
- )
15909
- ] }),
15910
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full border border-grey-200 px-2.5 py-1 text-xs font-semibold text-muted-foreground dark:border-grey-700", children: [
15911
- /* @__PURE__ */ jsxRuntime.jsx(Icons.contrast, { "data-slot": "icon", className: "size-4" }),
15912
- pair.rating
15913
- ] })
16094
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
16095
+ /* @__PURE__ */ jsxRuntime.jsx(PairDetailCard, { color: selectedBackground, format, role: "Background" }),
16096
+ detailForeground ? /* @__PURE__ */ jsxRuntime.jsx(PairDetailCard, { color: detailForeground, format, role: "Foreground" }) : /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "gap-4", children: [
16097
+ /* @__PURE__ */ jsxRuntime.jsx(CardHeader, { className: "gap-3 border-b", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1", children: [
16098
+ /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { children: "Foreground" }),
16099
+ /* @__PURE__ */ jsxRuntime.jsx(CardDescription, { children: "No recommended foreground available" })
16100
+ ] }) }),
16101
+ /* @__PURE__ */ jsxRuntime.jsx(CardContent, { children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "Select another approved tone or review the recommended foregrounds for the same background." }) })
16102
+ ] })
16103
+ ] }),
16104
+ selectedPair ? /* @__PURE__ */ jsxRuntime.jsx(PairComplianceCard, { pair: selectedPair }) : null,
16105
+ showWhiteForegroundExample && whiteForegroundExample ? /* @__PURE__ */ jsxRuntime.jsx(WhiteTextExampleCard, { pair: whiteForegroundExample }) : null
16106
+ ] }),
16107
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: "h-fit", children: [
16108
+ /* @__PURE__ */ jsxRuntime.jsxs(CardHeader, { className: "gap-2 border-b", children: [
16109
+ /* @__PURE__ */ jsxRuntime.jsx(CardTitle, { className: "text-base", children: "Recommended foregrounds" }),
16110
+ /* @__PURE__ */ jsxRuntime.jsxs(CardDescription, { children: [
16111
+ "AAA combinations for ",
16112
+ selectedBackground.token,
16113
+ ", drawn from",
16114
+ " ",
16115
+ context.primary.label,
16116
+ ", ",
16117
+ context.accent.label,
16118
+ ", Grey, and white using only foreground tones 50, 200, 400, 600, and 800."
16119
+ ] })
16120
+ ] }),
16121
+ /* @__PURE__ */ jsxRuntime.jsx(CardContent, { className: "grid gap-3", children: selectedBackgroundPairs.length > 0 ? selectedBackgroundPairs.map((pair) => {
16122
+ const isActive = selectedPair?.id === pair.id;
16123
+ return /* @__PURE__ */ jsxRuntime.jsxs(
16124
+ "button",
16125
+ {
16126
+ type: "button",
16127
+ "aria-label": `Use ${pair.foreground.token} on ${pair.background.token}`,
16128
+ "aria-pressed": isActive,
16129
+ onClick: () => handlePairChange(pair.id),
16130
+ className: cn(
16131
+ "rounded-sm border p-4 text-left transition-colors",
16132
+ isActive ? "border-primary-500 bg-primary-50/70 dark:bg-primary-950/30" : "border-grey-200 hover:border-grey-400 hover:bg-grey-50 dark:border-grey-700 dark:hover:border-grey-500 dark:hover:bg-grey-900/70"
16133
+ ),
16134
+ children: [
16135
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-4", children: [
16136
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
16137
+ /* @__PURE__ */ jsxRuntime.jsx(
16138
+ "div",
16139
+ {
16140
+ className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
16141
+ style: { backgroundColor: pair.background.hex }
16142
+ }
16143
+ ),
16144
+ /* @__PURE__ */ jsxRuntime.jsx(
16145
+ "div",
16146
+ {
16147
+ className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
16148
+ style: { backgroundColor: pair.foreground.hex }
16149
+ }
16150
+ )
15914
16151
  ] }),
15915
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 space-y-1", children: [
15916
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "font-medium text-foreground", children: [
15917
- pair.background.token,
15918
- " / ",
15919
- pair.foreground.token
15920
- ] }),
15921
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
15922
- pair.contrastRatio.toFixed(2),
15923
- ":1 contrast ratio"
15924
- ] })
16152
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-1 rounded-full border border-grey-200 px-2.5 py-1 text-xs font-semibold text-muted-foreground dark:border-grey-700", children: [
16153
+ /* @__PURE__ */ jsxRuntime.jsx(Icons.contrast, { "data-slot": "icon", className: "size-4" }),
16154
+ pair.rating
15925
16155
  ] })
15926
- ]
15927
- },
15928
- pair.id
15929
- );
15930
- }) })
15931
- ] }) })
15932
- ] }),
15933
- selectedPair ? /* @__PURE__ */ jsxRuntime.jsx(PairComplianceCard, { pair: selectedPair }) : null,
15934
- showWhiteForegroundExample && whiteForegroundExample ? /* @__PURE__ */ jsxRuntime.jsx(WhiteTextExampleCard, { pair: whiteForegroundExample }) : null
15935
- ] })
16156
+ ] }),
16157
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4 space-y-1", children: [
16158
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "font-medium text-foreground", children: [
16159
+ pair.background.token,
16160
+ " / ",
16161
+ pair.foreground.token
16162
+ ] }),
16163
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
16164
+ pair.foreground.familyLabel,
16165
+ " on ",
16166
+ pair.background.familyLabel
16167
+ ] }),
16168
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
16169
+ pair.contrastRatio.toFixed(2),
16170
+ ":1 contrast ratio"
16171
+ ] })
16172
+ ] })
16173
+ ]
16174
+ },
16175
+ pair.id
16176
+ );
16177
+ }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-sm border border-grey-200 p-4 text-sm text-muted-foreground dark:border-grey-700", children: "No recommended AAA foregrounds are available for this background." }) })
16178
+ ] }) })
16179
+ ] }) })
15936
16180
  ] });
15937
16181
  }
15938
16182
  function ColorPairingTool() {
@@ -18420,7 +18664,7 @@ function FormMessage({ className, ...props }) {
18420
18664
 
18421
18665
  // package.json
18422
18666
  var package_default = {
18423
- version: "1.98.0"};
18667
+ version: "1.99.0"};
18424
18668
  var SluggerContext = React5__namespace.default.createContext(null);
18425
18669
  function flattenText(nodes) {
18426
18670
  if (nodes == null || typeof nodes === "boolean") return "";