@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.js CHANGED
@@ -14919,7 +14919,10 @@ function ColorCard({ name, token, hex: hex2, rgb: rgb2, hsl, oklch: oklch2, form
14919
14919
  ] }) });
14920
14920
  }
14921
14921
  var recommendedBackgroundTones = [50, 100, 200, 400, 600, 800];
14922
+ var recommendedForegroundTones = [50, 200, 400, 600, 800];
14922
14923
  var AAA_NORMAL_TEXT_RATIO = 7;
14924
+ var DEFAULT_PRIMARY_FAMILY_KEY = "blue";
14925
+ var DEFAULT_ACCENT_FAMILY_KEY = "red";
14923
14926
  var MIN_DARK_BACKGROUND_TONE_FOR_WHITE = 600;
14924
14927
  var WHITE_FOREGROUND = {
14925
14928
  token: "white",
@@ -14928,6 +14931,8 @@ var WHITE_FOREGROUND = {
14928
14931
  rgb: "rgb(255, 255, 255)",
14929
14932
  hsl: "hsl(0, 0%, 100%)",
14930
14933
  oklch: "oklch(1 0 0)",
14934
+ familyKey: "white",
14935
+ familyLabel: "White",
14931
14936
  tone: 0
14932
14937
  };
14933
14938
  function extractTone(token) {
@@ -14938,23 +14943,48 @@ function formatFamilyLabel(name, key) {
14938
14943
  if (key === "grey") return "Grey";
14939
14944
  return name.replace(/^NSW Aboriginal\s+/i, "").replace(/^NSW\s+/i, "").trim();
14940
14945
  }
14941
- function toPairingColor(color2) {
14946
+ function toPairingColor(color2, familyKey, familyLabel) {
14942
14947
  return {
14943
14948
  ...color2,
14949
+ familyKey,
14950
+ familyLabel,
14944
14951
  tone: extractTone(color2.token)
14945
14952
  };
14946
14953
  }
14947
- function getPreferredForegroundTone(backgroundTone) {
14948
- if (backgroundTone <= 200) return 800;
14949
- if (backgroundTone <= 500) return 700;
14950
- if (backgroundTone <= 650) return 250;
14951
- return 200;
14954
+ function buildPairingCollections() {
14955
+ return {
14956
+ brand: Object.entries(colors.brand).map(([key, palette]) => {
14957
+ const familyLabel = formatFamilyLabel(palette.name, key);
14958
+ return {
14959
+ key,
14960
+ label: familyLabel,
14961
+ paletteName: palette.name,
14962
+ colors: palette.colors.map((color2) => toPairingColor(color2, key, familyLabel))
14963
+ };
14964
+ }),
14965
+ aboriginal: Object.entries(colors.aboriginal).map(([key, palette]) => {
14966
+ const familyLabel = formatFamilyLabel(palette.name, key);
14967
+ return {
14968
+ key,
14969
+ label: familyLabel,
14970
+ paletteName: palette.name,
14971
+ colors: palette.colors.map((color2) => toPairingColor(color2, key, familyLabel))
14972
+ };
14973
+ })
14974
+ };
14952
14975
  }
14976
+ var pairingFamilyCollections = buildPairingCollections();
14953
14977
  function getPairRating(contrastRatio) {
14954
14978
  if (contrastRatio >= 7) return "AAA";
14955
14979
  if (contrastRatio >= 4.5) return "AA";
14956
14980
  return "AA Large";
14957
14981
  }
14982
+ function getPreferredForegroundTone(backgroundTone) {
14983
+ if (backgroundTone <= 200) return 800;
14984
+ if (backgroundTone <= 500) return 700;
14985
+ if (backgroundTone <= 650) return 250;
14986
+ return 200;
14987
+ }
14958
14988
  function isPreferredDirection(backgroundTone, foregroundTone, preferredForegroundTone) {
14959
14989
  if (preferredForegroundTone < backgroundTone) {
14960
14990
  return foregroundTone < backgroundTone;
@@ -14977,15 +15007,27 @@ function buildPair(background, foreground) {
14977
15007
  rating: getPairRating(contrastRatio)
14978
15008
  };
14979
15009
  }
14980
- function pickForeground(colorsToPair, background, minimumRatio) {
15010
+ function sortRecommendedPairs(background, pairs) {
14981
15011
  const preferredForegroundTone = getPreferredForegroundTone(background.tone);
14982
- const passingCandidates = colorsToPair.filter((color2) => color2.token !== background.token).map((color2) => buildPair(background, color2)).filter((pair) => pair.contrastRatio >= minimumRatio);
14983
- if (passingCandidates.length === 0) return null;
14984
- const preferredCandidates = passingCandidates.filter(
14985
- (pair) => isPreferredDirection(background.tone, pair.foreground.tone, preferredForegroundTone)
14986
- );
14987
- const candidates = preferredCandidates.length > 0 ? preferredCandidates : passingCandidates;
14988
- return candidates.sort((left, right) => {
15012
+ return [...pairs].sort((left, right) => {
15013
+ const leftPreferred = isPreferredDirection(
15014
+ background.tone,
15015
+ left.foreground.tone,
15016
+ preferredForegroundTone
15017
+ ) ? 0 : 1;
15018
+ const rightPreferred = isPreferredDirection(
15019
+ background.tone,
15020
+ right.foreground.tone,
15021
+ preferredForegroundTone
15022
+ ) ? 0 : 1;
15023
+ if (leftPreferred !== rightPreferred) {
15024
+ return leftPreferred - rightPreferred;
15025
+ }
15026
+ const leftWhitePenalty = left.foreground.token === WHITE_FOREGROUND.token ? 1 : 0;
15027
+ const rightWhitePenalty = right.foreground.token === WHITE_FOREGROUND.token ? 1 : 0;
15028
+ if (leftWhitePenalty !== rightWhitePenalty) {
15029
+ return leftWhitePenalty - rightWhitePenalty;
15030
+ }
14989
15031
  const leftTargetDelta = Math.abs(left.foreground.tone - preferredForegroundTone);
14990
15032
  const rightTargetDelta = Math.abs(right.foreground.tone - preferredForegroundTone);
14991
15033
  if (leftTargetDelta !== rightTargetDelta) {
@@ -14996,49 +15038,116 @@ function pickForeground(colorsToPair, background, minimumRatio) {
14996
15038
  if (leftToneGap !== rightToneGap) {
14997
15039
  return rightToneGap - leftToneGap;
14998
15040
  }
14999
- return right.contrastRatio - left.contrastRatio;
15000
- })[0];
15001
- }
15002
- function buildRecommendedPairs(colorsToPair, minimumRatio) {
15003
- const backgrounds = recommendedBackgroundTones.map((tone) => colorsToPair.find((color2) => color2.tone === tone)).filter((color2) => Boolean(color2));
15004
- const recommendedPairs = [];
15005
- for (const background of backgrounds) {
15006
- const pair = pickForeground(colorsToPair, background, minimumRatio);
15007
- const whitePair = background.tone >= MIN_DARK_BACKGROUND_TONE_FOR_WHITE ? buildPair(background, WHITE_FOREGROUND) : null;
15008
- if (pair && !recommendedPairs.some((item) => item.id === pair.id)) {
15009
- recommendedPairs.push(pair);
15041
+ if (left.foreground.familyKey === background.familyKey && right.foreground.familyKey !== background.familyKey) {
15042
+ return -1;
15010
15043
  }
15011
- if (whitePair && whitePair.contrastRatio >= minimumRatio && !recommendedPairs.some((item) => item.id === whitePair.id)) {
15012
- recommendedPairs.push(whitePair);
15044
+ if (right.foreground.familyKey === background.familyKey && left.foreground.familyKey !== background.familyKey) {
15045
+ return 1;
15013
15046
  }
15047
+ return right.contrastRatio - left.contrastRatio;
15048
+ });
15049
+ }
15050
+ function dedupeColors(colorsToDedupe) {
15051
+ return Array.from(new Map(colorsToDedupe.map((color2) => [color2.token, color2])).values());
15052
+ }
15053
+ function buildRecommendedPairsForBackground(background, foregrounds, minimumRatio) {
15054
+ const candidatePairs = foregrounds.filter((foreground) => foreground.token !== background.token).map((foreground) => buildPair(background, foreground)).filter((pair) => pair.contrastRatio >= minimumRatio);
15055
+ const whitePair = background.tone >= MIN_DARK_BACKGROUND_TONE_FOR_WHITE ? buildPair(background, WHITE_FOREGROUND) : null;
15056
+ if (whitePair && whitePair.contrastRatio >= minimumRatio) {
15057
+ candidatePairs.push(whitePair);
15014
15058
  }
15015
- return recommendedPairs;
15059
+ const dedupedPairs = Array.from(new Map(candidatePairs.map((pair) => [pair.id, pair])).values());
15060
+ return sortRecommendedPairs(background, dedupedPairs);
15061
+ }
15062
+ function findGreyFamily(families) {
15063
+ return families.find((family) => family.key.toLowerCase().includes("grey")) ?? families[0];
15064
+ }
15065
+ function getNonGreyFamilies(families) {
15066
+ const greyFamily = findGreyFamily(families);
15067
+ return families.filter((family) => family.key !== greyFamily.key);
15068
+ }
15069
+ function findFamilyByKey(families, key) {
15070
+ return key ? families.find((family) => family.key === key) : void 0;
15071
+ }
15072
+ function getDefaultPrimaryFamily(families) {
15073
+ const nonGreyFamilies = getNonGreyFamilies(families);
15074
+ return findFamilyByKey(nonGreyFamilies, DEFAULT_PRIMARY_FAMILY_KEY) ?? findFamilyByKey(nonGreyFamilies, "green") ?? nonGreyFamilies[0] ?? families[0];
15016
15075
  }
15017
- function buildRecommendedCollections(minimumRatio) {
15076
+ function getDefaultAccentFamily(families, primaryKey) {
15077
+ const nonGreyFamilies = getNonGreyFamilies(families).filter((family) => family.key !== primaryKey);
15078
+ return findFamilyByKey(nonGreyFamilies, DEFAULT_ACCENT_FAMILY_KEY) ?? nonGreyFamilies[0] ?? getDefaultPrimaryFamily(families);
15079
+ }
15080
+ function isApprovedBackgroundTone(color2) {
15081
+ return recommendedBackgroundTones.includes(
15082
+ color2.tone
15083
+ );
15084
+ }
15085
+ function isApprovedForegroundTone(color2) {
15086
+ return recommendedForegroundTones.includes(
15087
+ color2.tone
15088
+ );
15089
+ }
15090
+ function getPairingFamilies(themeCategory) {
15091
+ return pairingFamilyCollections[themeCategory];
15092
+ }
15093
+ function getDefaultAccentFamilyKey(themeCategory, primaryKey) {
15094
+ return getDefaultAccentFamily(getPairingFamilies(themeCategory), primaryKey).key;
15095
+ }
15096
+ function getPairingContext(themeCategory, primaryKey, accentKey) {
15097
+ const families = getPairingFamilies(themeCategory);
15098
+ const primary = findFamilyByKey(getNonGreyFamilies(families), primaryKey) ?? getDefaultPrimaryFamily(families);
15099
+ const accent = findFamilyByKey(
15100
+ getNonGreyFamilies(families).filter((family) => family.key !== primary.key),
15101
+ accentKey
15102
+ ) ?? getDefaultAccentFamily(families, primary.key);
15103
+ const grey = findGreyFamily(families);
15104
+ const allFamilies = [primary, accent, grey];
15105
+ const backgroundGroups = [
15106
+ {
15107
+ key: "primary",
15108
+ label: "Primary backgrounds",
15109
+ family: primary,
15110
+ backgrounds: primary.colors.filter(isApprovedBackgroundTone)
15111
+ },
15112
+ {
15113
+ key: "accent",
15114
+ label: "Accent backgrounds",
15115
+ family: accent,
15116
+ backgrounds: accent.colors.filter(isApprovedBackgroundTone)
15117
+ },
15118
+ {
15119
+ key: "grey",
15120
+ label: "Grey backgrounds",
15121
+ family: grey,
15122
+ backgrounds: grey.colors.filter(isApprovedBackgroundTone)
15123
+ }
15124
+ ];
15125
+ const backgrounds = backgroundGroups.flatMap((group) => group.backgrounds);
15126
+ const candidateForegrounds = dedupeColors(
15127
+ allFamilies.flatMap((family) => family.colors.filter(isApprovedForegroundTone))
15128
+ );
15129
+ const pairsByBackground = {};
15130
+ for (const background of backgrounds) {
15131
+ pairsByBackground[background.token] = buildRecommendedPairsForBackground(
15132
+ background,
15133
+ candidateForegrounds,
15134
+ AAA_NORMAL_TEXT_RATIO
15135
+ );
15136
+ }
15018
15137
  return {
15019
- brand: Object.entries(colors.brand).map(([key, palette]) => {
15020
- const scale2 = palette.colors.map(toPairingColor);
15021
- return {
15022
- key,
15023
- label: formatFamilyLabel(palette.name, key),
15024
- paletteName: palette.name,
15025
- colors: scale2,
15026
- recommendedPairs: buildRecommendedPairs(scale2, minimumRatio)
15027
- };
15028
- }),
15029
- aboriginal: Object.entries(colors.aboriginal).map(([key, palette]) => {
15030
- const scale2 = palette.colors.map(toPairingColor);
15031
- return {
15032
- key,
15033
- label: formatFamilyLabel(palette.name, key),
15034
- paletteName: palette.name,
15035
- colors: scale2,
15036
- recommendedPairs: buildRecommendedPairs(scale2, minimumRatio)
15037
- };
15038
- })
15138
+ accent,
15139
+ allFamilies,
15140
+ backgroundGroups,
15141
+ backgrounds,
15142
+ grey,
15143
+ pairsByBackground,
15144
+ primary,
15145
+ recommendedPairs: backgrounds.flatMap(
15146
+ (background) => pairsByBackground[background.token] ?? []
15147
+ ),
15148
+ themeCategory
15039
15149
  };
15040
15150
  }
15041
- var recommendedAaaPairingCollections = buildRecommendedCollections(AAA_NORMAL_TEXT_RATIO);
15042
15151
  function getWhiteForegroundPair(background) {
15043
15152
  return buildPair(background, WHITE_FOREGROUND);
15044
15153
  }
@@ -15190,64 +15299,86 @@ function FormatToggle({ format, setFormat }) {
15190
15299
  )
15191
15300
  ] });
15192
15301
  }
15193
- function getPreferredFamilyKey(families) {
15194
- return families.find((family) => family.key === "green")?.key ?? families[0]?.key ?? "";
15195
- }
15196
- function getDefaultPair(family) {
15197
- if (!family || family.recommendedPairs.length === 0) return null;
15198
- return family.recommendedPairs.reduce((bestPair, pair) => {
15199
- const bestDelta = Math.abs(bestPair.background.tone - 600);
15200
- const currentDelta = Math.abs(pair.background.tone - 600);
15201
- return currentDelta < bestDelta ? pair : bestPair;
15202
- }, family.recommendedPairs[0]);
15203
- }
15204
- function getFamilySwatchColor(family) {
15205
- 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";
15206
- }
15302
+ var AAA_NORMAL_TEXT_THRESHOLD = "7.0:1 or higher for text below 18pt, or below 14pt bold";
15303
+ var AAA_LARGE_TEXT_THRESHOLD = "4.5:1 or higher for text at 18pt and above, or 14pt and above bold";
15304
+ var PREFERRED_BACKGROUND_TONES = [400, 600, 200, 800, 100, 50];
15207
15305
  function getToneFromToken(token) {
15208
15306
  if (!token) return null;
15209
15307
  const match = token.match(/-(\d+)$/);
15210
15308
  return match ? Number.parseInt(match[1], 10) : null;
15211
15309
  }
15212
- function isApprovedBackgroundTone(color2) {
15213
- return recommendedBackgroundTones.includes(
15214
- color2.tone
15215
- );
15310
+ function getFamilySwatchColor(family, preferredTone = 600) {
15311
+ const exactMatch = family.colors.find((color2) => color2.tone === preferredTone);
15312
+ if (exactMatch) {
15313
+ return exactMatch.hex;
15314
+ }
15315
+ const closestMatch = [...family.colors].sort(
15316
+ (left, right) => Math.abs(left.tone - preferredTone) - Math.abs(right.tone - preferredTone)
15317
+ )[0];
15318
+ return closestMatch?.hex ?? "transparent";
15216
15319
  }
15217
- function getBackgroundOptions(family) {
15218
- return family?.colors.filter(isApprovedBackgroundTone) ?? [];
15320
+ function getFamilySelectorLabel(family, themeCategory, selectionRole) {
15321
+ if (themeCategory !== "aboriginal") {
15322
+ return family.label;
15323
+ }
15324
+ const preferredTone = selectionRole === "primary colour" ? 800 : 600;
15325
+ return family.colors.find((color2) => color2.tone === preferredTone)?.name ?? family.label;
15219
15326
  }
15220
15327
  function isWhiteForegroundPair(pair) {
15221
15328
  return pair.foreground.token === "white";
15222
15329
  }
15223
- function getRecommendedPairForBackground(family, backgroundToken, preferredPairId) {
15224
- if (!family) return null;
15225
- const backgroundPairs = family.recommendedPairs.filter(
15226
- (pair) => pair.background.token === backgroundToken
15227
- );
15228
- if (backgroundPairs.length === 0) return null;
15330
+ function getPreferredPairForBackground(pairs, preferredPairId) {
15229
15331
  if (preferredPairId) {
15230
- const preferredPair = backgroundPairs.find((pair) => pair.id === preferredPairId);
15332
+ const preferredPair = pairs.find((pair) => pair.id === preferredPairId);
15231
15333
  if (preferredPair) {
15232
15334
  return preferredPair;
15233
15335
  }
15234
15336
  }
15235
- return backgroundPairs.find((pair) => !isWhiteForegroundPair(pair)) ?? backgroundPairs[0];
15337
+ return pairs.find((pair) => !isWhiteForegroundPair(pair)) ?? pairs[0] ?? null;
15236
15338
  }
15237
- function getFallbackBackgroundToken(family, preferredTone) {
15238
- const backgroundOptions = getBackgroundOptions(family);
15239
- if (preferredTone) {
15240
- const preferredBackground = backgroundOptions.find((color2) => color2.tone === preferredTone);
15241
- if (preferredBackground) {
15242
- return preferredBackground.token;
15339
+ function getDefaultBackgroundToken(context) {
15340
+ for (const tone of PREFERRED_BACKGROUND_TONES) {
15341
+ for (const group of context.backgroundGroups) {
15342
+ const match = group.backgrounds.find(
15343
+ (background) => background.tone === tone && (context.pairsByBackground[background.token]?.length ?? 0) > 0
15344
+ );
15345
+ if (match) {
15346
+ return match.token;
15347
+ }
15348
+ }
15349
+ }
15350
+ for (const tone of PREFERRED_BACKGROUND_TONES) {
15351
+ for (const group of context.backgroundGroups) {
15352
+ const match = group.backgrounds.find((background) => background.tone === tone);
15353
+ if (match) {
15354
+ return match.token;
15355
+ }
15356
+ }
15357
+ }
15358
+ return context.backgrounds[0]?.token ?? "";
15359
+ }
15360
+ function resolveBackgroundToken(context, preferredToken, preferredTone) {
15361
+ if (preferredToken && context.backgrounds.some((background) => background.token === preferredToken)) {
15362
+ return preferredToken;
15363
+ }
15364
+ if (preferredTone !== null && preferredTone !== void 0) {
15365
+ for (const group of context.backgroundGroups) {
15366
+ const match = group.backgrounds.find((background) => background.tone === preferredTone);
15367
+ if (match) {
15368
+ return match.token;
15369
+ }
15243
15370
  }
15244
15371
  }
15245
- return getDefaultPair(family)?.background.token ?? backgroundOptions[0]?.token ?? "";
15372
+ return getDefaultBackgroundToken(context);
15246
15373
  }
15247
15374
  function ColorPairingToolLoading() {
15248
15375
  return /* @__PURE__ */ jsxs("div", { className: "animate-pulse space-y-6", children: [
15249
- /* @__PURE__ */ jsx("div", { className: "h-40 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15250
- /* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1.3fr)_minmax(20rem,0.9fr)]", children: [
15376
+ /* @__PURE__ */ jsx("div", { className: "h-32 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15377
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-6 lg:grid-cols-2", children: [
15378
+ /* @__PURE__ */ jsx("div", { className: "h-48 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15379
+ /* @__PURE__ */ jsx("div", { className: "h-48 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" })
15380
+ ] }),
15381
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(18rem,0.72fr)]", children: [
15251
15382
  /* @__PURE__ */ jsx("div", { className: "h-96 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" }),
15252
15383
  /* @__PURE__ */ jsx("div", { className: "h-96 rounded-sm border border-grey-200 bg-grey-50 dark:border-grey-700 dark:bg-grey-900" })
15253
15384
  ] })
@@ -15255,24 +15386,32 @@ function ColorPairingToolLoading() {
15255
15386
  }
15256
15387
  function getInitialPairingState(searchParams) {
15257
15388
  const paletteParam = searchParams.get("palette");
15258
- const familyParam = searchParams.get("family");
15389
+ const primaryParam = searchParams.get("primary");
15390
+ const accentParam = searchParams.get("accent");
15259
15391
  const pairParam = searchParams.get("pair");
15260
15392
  const backgroundParam = searchParams.get("background");
15261
15393
  const themeCategory = paletteParam === "brand" || paletteParam === "aboriginal" ? paletteParam : "brand";
15262
- const families = recommendedAaaPairingCollections[themeCategory];
15263
- const familyKey = families.some((family) => family.key === familyParam) ? familyParam : getPreferredFamilyKey(families);
15264
- const activeFamily = families.find((family) => family.key === familyKey) ?? families[0];
15265
- const backgroundOptions = getBackgroundOptions(activeFamily);
15266
- const pairBackgroundToken = activeFamily?.recommendedPairs.find((pair) => pair.id === pairParam)?.background.token ?? null;
15267
- const selectedBackgroundToken = backgroundOptions.some((color2) => color2.token === backgroundParam) ? backgroundParam : getFallbackBackgroundToken(
15268
- activeFamily,
15394
+ const context = getPairingContext(themeCategory, primaryParam, accentParam);
15395
+ const pairBackgroundToken = context.recommendedPairs.find((pair) => pair.id === pairParam)?.background.token ?? null;
15396
+ const selectedBackgroundToken = resolveBackgroundToken(
15397
+ context,
15398
+ backgroundParam ?? pairBackgroundToken,
15269
15399
  getToneFromToken(backgroundParam ?? pairBackgroundToken)
15270
15400
  );
15271
- const selectedPairId = getRecommendedPairForBackground(activeFamily, selectedBackgroundToken, pairParam)?.id ?? "";
15272
- return { familyKey, selectedBackgroundToken, selectedPairId, themeCategory };
15401
+ const selectedPairId = getPreferredPairForBackground(
15402
+ context.pairsByBackground[selectedBackgroundToken] ?? [],
15403
+ pairParam
15404
+ )?.id ?? "";
15405
+ return {
15406
+ accentKey: context.accent.key,
15407
+ primaryKey: context.primary.key,
15408
+ selectedBackgroundToken,
15409
+ selectedPairId,
15410
+ themeCategory
15411
+ };
15273
15412
  }
15274
15413
  function PairPreview({
15275
- family,
15414
+ familySummary,
15276
15415
  isRecommended,
15277
15416
  pair
15278
15417
  }) {
@@ -15300,7 +15439,7 @@ function PairPreview({
15300
15439
  },
15301
15440
  children: [
15302
15441
  /* @__PURE__ */ jsx(Icons.palette, { "data-slot": "icon", className: "size-4" }),
15303
- family.label
15442
+ familySummary
15304
15443
  ]
15305
15444
  }
15306
15445
  ),
@@ -15321,10 +15460,10 @@ function PairPreview({
15321
15460
  )
15322
15461
  ] }),
15323
15462
  /* @__PURE__ */ jsxs("div", { className: "max-w-xl space-y-4 pb-12 sm:pb-16", children: [
15324
- /* @__PURE__ */ 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" }),
15463
+ /* @__PURE__ */ 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" }),
15325
15464
  /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
15326
15465
  /* @__PURE__ */ jsx("h3", { className: "max-w-lg text-4xl leading-none font-bold text-current sm:text-5xl", children: "Pair colour with confidence." }),
15327
- /* @__PURE__ */ 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." })
15466
+ /* @__PURE__ */ 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." })
15328
15467
  ] })
15329
15468
  ] }),
15330
15469
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
@@ -15361,6 +15500,41 @@ function PairPreview({
15361
15500
  }
15362
15501
  ) });
15363
15502
  }
15503
+ function PreviewFallbackCard({
15504
+ familySummary,
15505
+ selectedBackground
15506
+ }) {
15507
+ return /* @__PURE__ */ jsx(Card, { className: "gap-0 overflow-hidden py-0", children: /* @__PURE__ */ jsx(
15508
+ "div",
15509
+ {
15510
+ className: "p-6 sm:min-h-[26rem] sm:p-8",
15511
+ style: {
15512
+ backgroundColor: selectedBackground.hex,
15513
+ color: selectedBackground.tone >= 600 ? "#ffffff" : "#002664"
15514
+ },
15515
+ children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-[22rem] flex-col justify-between gap-8", children: [
15516
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: /* @__PURE__ */ 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: [
15517
+ /* @__PURE__ */ jsx(Icons.palette, { "data-slot": "icon", className: "size-4" }),
15518
+ familySummary
15519
+ ] }) }),
15520
+ /* @__PURE__ */ jsxs("div", { className: "max-w-xl space-y-4 pb-12 sm:pb-16", children: [
15521
+ /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold tracking-[0.22em] uppercase", children: "Approved background" }),
15522
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
15523
+ /* @__PURE__ */ jsx("h3", { className: "max-w-lg text-4xl leading-none font-bold text-current sm:text-5xl", children: "Pair colour with confidence." }),
15524
+ /* @__PURE__ */ 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." })
15525
+ ] })
15526
+ ] }),
15527
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
15528
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 rounded-full bg-current/10 px-4 py-2 text-sm font-semibold", children: [
15529
+ "No recommendation",
15530
+ /* @__PURE__ */ jsx(Icons.info, { "data-slot": "icon", className: "size-4" })
15531
+ ] }),
15532
+ /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full border border-current px-4 py-2 text-sm", children: selectedBackground.token })
15533
+ ] })
15534
+ ] })
15535
+ }
15536
+ ) });
15537
+ }
15364
15538
  function PairDetailCard({
15365
15539
  color: color2,
15366
15540
  format,
@@ -15476,7 +15650,7 @@ function PairComplianceCard({ pair }) {
15476
15650
  {
15477
15651
  label: "AAA normal text",
15478
15652
  passes: pair.passes.aaaText,
15479
- threshold: "7.0:1 or higher"
15653
+ threshold: AAA_NORMAL_TEXT_THRESHOLD
15480
15654
  }
15481
15655
  ),
15482
15656
  /* @__PURE__ */ jsx(
@@ -15484,7 +15658,7 @@ function PairComplianceCard({ pair }) {
15484
15658
  {
15485
15659
  label: "AAA large text",
15486
15660
  passes: pair.passes.aaaLarge,
15487
- threshold: "4.5:1 or higher"
15661
+ threshold: AAA_LARGE_TEXT_THRESHOLD
15488
15662
  }
15489
15663
  )
15490
15664
  ] })
@@ -15522,7 +15696,8 @@ function WhiteTextExampleCard({ pair }) {
15522
15696
  "AAA large text: ",
15523
15697
  /* @__PURE__ */ jsx("strong", { children: pair.passes.aaaLarge ? "pass" : "fail" })
15524
15698
  ] })
15525
- ] })
15699
+ ] }),
15700
+ /* @__PURE__ */ 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." })
15526
15701
  ] })
15527
15702
  }
15528
15703
  ),
@@ -15539,7 +15714,7 @@ function WhiteTextExampleCard({ pair }) {
15539
15714
  {
15540
15715
  label: "AAA normal text",
15541
15716
  passes: pair.passes.aaaText,
15542
- threshold: "7.0:1 or higher"
15717
+ threshold: AAA_NORMAL_TEXT_THRESHOLD
15543
15718
  }
15544
15719
  ),
15545
15720
  /* @__PURE__ */ jsx(
@@ -15547,42 +15722,90 @@ function WhiteTextExampleCard({ pair }) {
15547
15722
  {
15548
15723
  label: "AAA large text",
15549
15724
  passes: pair.passes.aaaLarge,
15550
- threshold: "4.5:1 or higher"
15725
+ threshold: AAA_LARGE_TEXT_THRESHOLD
15551
15726
  }
15552
15727
  )
15553
15728
  ] })
15554
15729
  ] })
15555
15730
  ] });
15556
15731
  }
15732
+ function ColorFamilySelector({
15733
+ families,
15734
+ label,
15735
+ selectedKey,
15736
+ selectionRole,
15737
+ themeCategory,
15738
+ onSelect
15739
+ }) {
15740
+ const swatchTone = selectionRole === "primary colour" ? 800 : 600;
15741
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
15742
+ /* @__PURE__ */ jsx("h3", { className: "font-medium", children: label }),
15743
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2 lg:grid-cols-4 xl:grid-cols-5", children: families.map((family) => {
15744
+ const isSelected = family.key === selectedKey;
15745
+ const familySelectorLabel = getFamilySelectorLabel(family, themeCategory, selectionRole);
15746
+ return /* @__PURE__ */ jsxs(
15747
+ "button",
15748
+ {
15749
+ type: "button",
15750
+ "aria-label": `Select ${familySelectorLabel} as ${selectionRole}`,
15751
+ "aria-pressed": isSelected,
15752
+ onClick: () => onSelect(family.key),
15753
+ className: cn(
15754
+ "group relative flex w-full items-center gap-3 rounded-sm border px-3 py-2 text-left transition-colors",
15755
+ "border-grey-200 bg-background hover:bg-primary-800/10 hover:text-foreground dark:hover:bg-white/10",
15756
+ isSelected && "border-grey-800 dark:border-grey-400"
15757
+ ),
15758
+ children: [
15759
+ /* @__PURE__ */ jsx(
15760
+ "span",
15761
+ {
15762
+ className: "size-4 shrink-0 rounded-full",
15763
+ style: { backgroundColor: getFamilySwatchColor(family, swatchTone) }
15764
+ }
15765
+ ),
15766
+ /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: familySelectorLabel })
15767
+ ]
15768
+ },
15769
+ family.key
15770
+ );
15771
+ }) })
15772
+ ] });
15773
+ }
15557
15774
  function ColorPairingToolContent() {
15558
15775
  const searchParams = useSearchParams();
15559
15776
  const {
15560
- familyKey: initialFamilyKey,
15777
+ accentKey: initialAccentKey,
15778
+ primaryKey: initialPrimaryKey,
15561
15779
  selectedBackgroundToken: initialSelectedBackgroundToken,
15562
15780
  selectedPairId: initialSelectedPairId,
15563
15781
  themeCategory: initialThemeCategory
15564
15782
  } = getInitialPairingState(searchParams);
15565
15783
  const [themeCategory, setThemeCategory] = useState(initialThemeCategory);
15566
15784
  const [format, setFormat] = useState("hex");
15567
- const [activeFamilyKey, setActiveFamilyKey] = useState(initialFamilyKey);
15785
+ const [primaryFamilyKey, setPrimaryFamilyKey] = useState(initialPrimaryKey);
15786
+ const [accentFamilyKey, setAccentFamilyKey] = useState(initialAccentKey);
15568
15787
  const [selectedBackgroundToken, setSelectedBackgroundToken] = useState(
15569
15788
  initialSelectedBackgroundToken
15570
15789
  );
15571
15790
  const [selectedPairId, setSelectedPairId] = useState(initialSelectedPairId);
15572
- const families = recommendedAaaPairingCollections[themeCategory];
15573
- const resolvedActiveFamilyKey = families.some((family) => family.key === activeFamilyKey) ? activeFamilyKey : getPreferredFamilyKey(families);
15574
- const activeFamily = families.find((family) => family.key === resolvedActiveFamilyKey) ?? families[0];
15575
- const backgroundOptions = getBackgroundOptions(activeFamily);
15576
- const selectedBackground = backgroundOptions.find((color2) => color2.token === selectedBackgroundToken) ?? backgroundOptions[0] ?? activeFamily?.colors[0] ?? null;
15577
- const selectedPair = selectedBackground ? getRecommendedPairForBackground(activeFamily, selectedBackground.token, selectedPairId) : null;
15791
+ const themeFamilies = getPairingFamilies(themeCategory);
15792
+ const context = getPairingContext(themeCategory, primaryFamilyKey, accentFamilyKey);
15793
+ const selectableFamilies = themeFamilies.filter((family) => family.key !== context.grey.key);
15794
+ const selectedBackground = context.backgrounds.find((background) => background.token === selectedBackgroundToken) ?? context.backgrounds[0] ?? null;
15795
+ const selectedBackgroundPairs = selectedBackground ? context.pairsByBackground[selectedBackground.token] ?? [] : [];
15796
+ const selectedPair = getPreferredPairForBackground(selectedBackgroundPairs, selectedPairId);
15578
15797
  const whiteForegroundExample = selectedBackground && supportsWhiteForegroundPreview(selectedBackground) ? getWhiteForegroundPair(selectedBackground) : null;
15579
- const previewPair = selectedPair ?? whiteForegroundExample;
15798
+ const previewPair = selectedPair ?? whiteForegroundExample ?? null;
15580
15799
  const showWhiteForegroundExample = Boolean(whiteForegroundExample) && (!selectedPair || selectedPair.foreground.token !== "white");
15581
15800
  const detailForeground = selectedPair?.foreground ?? whiteForegroundExample?.foreground ?? null;
15582
- const updateUrlParams = (nextThemeCategory, nextFamilyKey, nextSelectedBackgroundToken, nextSelectedPairId) => {
15801
+ const familySummary = [context.primary.label, context.accent.label, context.grey.label].join(
15802
+ " + "
15803
+ );
15804
+ const updateUrlParams = (nextThemeCategory, nextPrimaryKey, nextAccentKey, nextSelectedBackgroundToken, nextSelectedPairId) => {
15583
15805
  const params = new URLSearchParams(window.location.search);
15584
15806
  params.set("palette", nextThemeCategory);
15585
- params.set("family", nextFamilyKey);
15807
+ params.set("primary", nextPrimaryKey);
15808
+ params.set("accent", nextAccentKey);
15586
15809
  params.set("background", nextSelectedBackgroundToken);
15587
15810
  if (nextSelectedPairId) {
15588
15811
  params.set("pair", nextSelectedPairId);
@@ -15595,79 +15818,87 @@ function ColorPairingToolContent() {
15595
15818
  `${window.location.pathname}?${params.toString()}${window.location.hash}`
15596
15819
  );
15597
15820
  };
15598
- const handleThemeCategoryChange = (nextThemeCategory) => {
15599
- const nextFamilies = recommendedAaaPairingCollections[nextThemeCategory];
15600
- const nextFamilyKey = nextFamilies.some((family) => family.key === activeFamilyKey) ? activeFamilyKey : getPreferredFamilyKey(nextFamilies);
15601
- const nextActiveFamily = nextFamilies.find((family) => family.key === nextFamilyKey) ?? nextFamilies[0];
15602
- const nextSelectedBackgroundToken = getFallbackBackgroundToken(
15603
- nextActiveFamily,
15604
- getToneFromToken(selectedBackgroundToken)
15605
- );
15606
- const nextSelectedPair = getRecommendedPairForBackground(
15607
- nextActiveFamily,
15608
- nextSelectedBackgroundToken,
15609
- selectedPairId
15821
+ const syncSelection = (nextThemeCategory, nextPrimaryKey, nextAccentKey, preferredBackgroundToken, preferredPairId) => {
15822
+ const nextContext = getPairingContext(nextThemeCategory, nextPrimaryKey, nextAccentKey);
15823
+ const nextSelectedBackgroundToken = resolveBackgroundToken(
15824
+ nextContext,
15825
+ preferredBackgroundToken,
15826
+ getToneFromToken(preferredBackgroundToken)
15610
15827
  );
15828
+ const nextSelectedPairId = getPreferredPairForBackground(
15829
+ nextContext.pairsByBackground[nextSelectedBackgroundToken] ?? [],
15830
+ preferredPairId
15831
+ )?.id ?? "";
15611
15832
  setThemeCategory(nextThemeCategory);
15612
- setActiveFamilyKey(nextFamilyKey);
15833
+ setPrimaryFamilyKey(nextContext.primary.key);
15834
+ setAccentFamilyKey(nextContext.accent.key);
15613
15835
  setSelectedBackgroundToken(nextSelectedBackgroundToken);
15614
- setSelectedPairId(nextSelectedPair?.id ?? "");
15836
+ setSelectedPairId(nextSelectedPairId);
15615
15837
  updateUrlParams(
15616
15838
  nextThemeCategory,
15617
- nextFamilyKey,
15839
+ nextContext.primary.key,
15840
+ nextContext.accent.key,
15618
15841
  nextSelectedBackgroundToken,
15619
- nextSelectedPair?.id ?? ""
15842
+ nextSelectedPairId
15620
15843
  );
15621
15844
  };
15622
- const handleFamilyChange = (nextFamilyKey) => {
15623
- const nextActiveFamily = families.find((family) => family.key === nextFamilyKey);
15624
- const nextSelectedBackgroundToken = getFallbackBackgroundToken(
15625
- nextActiveFamily,
15626
- getToneFromToken(selectedBackgroundToken)
15845
+ const handleThemeCategoryChange = (nextThemeCategory) => {
15846
+ syncSelection(
15847
+ nextThemeCategory,
15848
+ primaryFamilyKey,
15849
+ accentFamilyKey,
15850
+ selectedBackgroundToken,
15851
+ selectedPairId
15627
15852
  );
15628
- const nextSelectedPair = getRecommendedPairForBackground(
15629
- nextActiveFamily,
15630
- nextSelectedBackgroundToken,
15853
+ };
15854
+ const handlePrimaryColorChange = (nextPrimaryKey) => {
15855
+ const nextAccentKey = nextPrimaryKey === accentFamilyKey ? getDefaultAccentFamilyKey(themeCategory, nextPrimaryKey) : accentFamilyKey;
15856
+ syncSelection(
15857
+ themeCategory,
15858
+ nextPrimaryKey,
15859
+ nextAccentKey,
15860
+ selectedBackgroundToken,
15631
15861
  selectedPairId
15632
15862
  );
15633
- setActiveFamilyKey(nextFamilyKey);
15634
- setSelectedBackgroundToken(nextSelectedBackgroundToken);
15635
- setSelectedPairId(nextSelectedPair?.id ?? "");
15636
- updateUrlParams(
15863
+ };
15864
+ const handleAccentColorChange = (nextAccentKey) => {
15865
+ if (nextAccentKey === primaryFamilyKey) return;
15866
+ syncSelection(
15637
15867
  themeCategory,
15638
- nextFamilyKey,
15639
- nextSelectedBackgroundToken,
15640
- nextSelectedPair?.id ?? ""
15868
+ primaryFamilyKey,
15869
+ nextAccentKey,
15870
+ selectedBackgroundToken,
15871
+ selectedPairId
15641
15872
  );
15642
15873
  };
15643
15874
  const handleBackgroundChange = (nextSelectedBackgroundToken) => {
15644
- const nextSelectedPair = getRecommendedPairForBackground(
15645
- activeFamily,
15646
- nextSelectedBackgroundToken,
15875
+ const nextSelectedPairId = getPreferredPairForBackground(
15876
+ context.pairsByBackground[nextSelectedBackgroundToken] ?? [],
15647
15877
  selectedPairId
15648
- );
15878
+ )?.id ?? "";
15649
15879
  setSelectedBackgroundToken(nextSelectedBackgroundToken);
15650
- setSelectedPairId(nextSelectedPair?.id ?? "");
15880
+ setSelectedPairId(nextSelectedPairId);
15651
15881
  updateUrlParams(
15652
15882
  themeCategory,
15653
- resolvedActiveFamilyKey,
15883
+ context.primary.key,
15884
+ context.accent.key,
15654
15885
  nextSelectedBackgroundToken,
15655
- nextSelectedPair?.id ?? ""
15886
+ nextSelectedPairId
15656
15887
  );
15657
15888
  };
15658
15889
  const handlePairChange = (nextSelectedPairId) => {
15659
- const nextSelectedPair = activeFamily?.recommendedPairs.find((pair) => pair.id === nextSelectedPairId) ?? null;
15660
- if (!nextSelectedPair) return;
15661
- setSelectedBackgroundToken(nextSelectedPair.background.token);
15890
+ const nextSelectedPair = selectedBackgroundPairs.find((pair) => pair.id === nextSelectedPairId);
15891
+ if (!nextSelectedPair || !selectedBackground) return;
15662
15892
  setSelectedPairId(nextSelectedPairId);
15663
15893
  updateUrlParams(
15664
15894
  themeCategory,
15665
- resolvedActiveFamilyKey,
15666
- nextSelectedPair.background.token,
15895
+ context.primary.key,
15896
+ context.accent.key,
15897
+ selectedBackground.token,
15667
15898
  nextSelectedPairId
15668
15899
  );
15669
15900
  };
15670
- if (!activeFamily || !selectedBackground) {
15901
+ if (!selectedBackground) {
15671
15902
  return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardHeader, { children: [
15672
15903
  /* @__PURE__ */ jsx(CardTitle, { children: "No approved background tones available" }),
15673
15904
  /* @__PURE__ */ jsx(CardDescription, { children: "No approved tones are available for the current palette." })
@@ -15691,194 +15922,207 @@ function ColorPairingToolContent() {
15691
15922
  ] }),
15692
15923
  /* @__PURE__ */ jsx(FormatToggle, { format, setFormat })
15693
15924
  ] }),
15694
- /* @__PURE__ */ jsx("div", { className: "space-y-3", children: /* @__PURE__ */ jsx("div", { className: "grid grid-cols-2 gap-2 lg:grid-cols-4 xl:grid-cols-5", children: families.map((family) => {
15695
- const isActive = family.key === resolvedActiveFamilyKey;
15696
- return /* @__PURE__ */ jsxs(
15697
- "button",
15925
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-6", children: [
15926
+ /* @__PURE__ */ jsx(
15927
+ ColorFamilySelector,
15928
+ {
15929
+ label: "Primary colour",
15930
+ families: selectableFamilies,
15931
+ selectedKey: context.primary.key,
15932
+ selectionRole: "primary colour",
15933
+ themeCategory,
15934
+ onSelect: handlePrimaryColorChange
15935
+ }
15936
+ ),
15937
+ /* @__PURE__ */ jsx(
15938
+ ColorFamilySelector,
15939
+ {
15940
+ label: "Accent colour",
15941
+ families: selectableFamilies.filter((family) => family.key !== context.primary.key),
15942
+ selectedKey: context.accent.key,
15943
+ selectionRole: "accent colour",
15944
+ themeCategory,
15945
+ onSelect: handleAccentColorChange
15946
+ }
15947
+ )
15948
+ ] }),
15949
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
15950
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: "Included families:" }),
15951
+ context.allFamilies.map((family) => /* @__PURE__ */ jsxs(
15952
+ "span",
15698
15953
  {
15699
- type: "button",
15700
- onClick: () => handleFamilyChange(family.key),
15701
- className: cn(
15702
- "group relative flex w-full items-center gap-3 rounded-sm border px-3 py-2 text-left transition-colors",
15703
- "border-grey-200 bg-background hover:bg-primary-800/10 hover:text-foreground dark:hover:bg-white/10",
15704
- isActive && "border-grey-800 dark:border-grey-400"
15705
- ),
15954
+ className: "inline-flex items-center gap-2 rounded-full border border-grey-200 px-3 py-1 text-muted-foreground dark:border-grey-700",
15706
15955
  children: [
15707
15956
  /* @__PURE__ */ jsx(
15708
15957
  "span",
15709
15958
  {
15710
- className: "size-4 shrink-0 rounded-full",
15959
+ className: "size-2.5 rounded-full",
15711
15960
  style: { backgroundColor: getFamilySwatchColor(family) }
15712
15961
  }
15713
15962
  ),
15714
- /* @__PURE__ */ jsx("span", { className: "truncate font-medium", children: family.label })
15963
+ family.label
15715
15964
  ]
15716
15965
  },
15717
15966
  family.key
15718
- );
15719
- }) }) }),
15720
- /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
15721
- /* @__PURE__ */ jsxs("div", { className: "grid items-start gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(17rem,0.68fr)]", children: [
15722
- /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
15723
- previewPair ? /* @__PURE__ */ jsx(
15724
- PairPreview,
15725
- {
15726
- family: activeFamily,
15727
- isRecommended: Boolean(selectedPair),
15728
- pair: previewPair
15729
- }
15730
- ) : /* @__PURE__ */ jsx(Card, { className: "gap-0 overflow-hidden py-0", children: /* @__PURE__ */ jsx(
15731
- "div",
15732
- {
15733
- className: "p-6 sm:min-h-[26rem] sm:p-8",
15734
- style: {
15735
- backgroundColor: selectedBackground.hex,
15736
- color: selectedBackground.tone >= 600 ? "#ffffff" : "#002664"
15737
- },
15738
- children: /* @__PURE__ */ jsxs("div", { className: "flex min-h-[22rem] flex-col justify-between gap-8", children: [
15739
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2", children: /* @__PURE__ */ 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: [
15740
- /* @__PURE__ */ jsx(Icons.palette, { "data-slot": "icon", className: "size-4" }),
15741
- activeFamily.label
15742
- ] }) }),
15743
- /* @__PURE__ */ jsxs("div", { className: "max-w-xl space-y-4 pb-12 sm:pb-16", children: [
15744
- /* @__PURE__ */ jsx("p", { className: "text-sm font-semibold tracking-[0.22em] uppercase", children: "Approved background" }),
15745
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
15746
- /* @__PURE__ */ jsx("h3", { className: "max-w-lg text-4xl leading-none font-bold text-current sm:text-5xl", children: "Pair colour with confidence." }),
15747
- /* @__PURE__ */ 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." })
15748
- ] })
15749
- ] }),
15750
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [
15751
- /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-2 rounded-full bg-current/10 px-4 py-2 text-sm font-semibold", children: [
15752
- "No recommendation",
15753
- /* @__PURE__ */ jsx(Icons.info, { "data-slot": "icon", className: "size-4" })
15754
- ] }),
15755
- /* @__PURE__ */ jsx("span", { className: "inline-flex rounded-full border border-current px-4 py-2 text-sm", children: selectedBackground.token })
15756
- ] })
15757
- ] })
15758
- }
15759
- ) }),
15760
- /* @__PURE__ */ jsxs(Card, { children: [
15761
- /* @__PURE__ */ jsxs(CardHeader, { className: "gap-2 border-b", children: [
15762
- /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Background tone strip" }),
15763
- /* @__PURE__ */ 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." })
15764
- ] }),
15765
- /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
15766
- /* @__PURE__ */ 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) => {
15767
- const pair = getRecommendedPairForBackground(activeFamily, color2.token);
15768
- const hasWhiteExample = supportsWhiteForegroundPreview(color2);
15769
- const isSelected = selectedBackground.token === color2.token;
15770
- return /* @__PURE__ */ jsxs(
15967
+ ))
15968
+ ] }),
15969
+ /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxs("div", { className: "grid items-start gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(18rem,0.72fr)]", children: [
15970
+ /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
15971
+ previewPair ? /* @__PURE__ */ jsx(
15972
+ PairPreview,
15973
+ {
15974
+ familySummary,
15975
+ isRecommended: Boolean(selectedPair),
15976
+ pair: previewPair
15977
+ }
15978
+ ) : /* @__PURE__ */ jsx(
15979
+ PreviewFallbackCard,
15980
+ {
15981
+ familySummary,
15982
+ selectedBackground
15983
+ }
15984
+ ),
15985
+ /* @__PURE__ */ jsxs(Card, { children: [
15986
+ /* @__PURE__ */ jsxs(CardHeader, { className: "gap-2 border-b", children: [
15987
+ /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Approved backgrounds" }),
15988
+ /* @__PURE__ */ 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." })
15989
+ ] }),
15990
+ /* @__PURE__ */ jsxs(CardContent, { className: "space-y-5", children: [
15991
+ context.backgroundGroups.map((group) => /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
15992
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2 text-sm", children: [
15993
+ /* @__PURE__ */ jsx("span", { className: "font-medium", children: group.label }),
15994
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: group.family.label })
15995
+ ] }),
15996
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 sm:grid-cols-6", children: group.backgrounds.map((background) => {
15997
+ const pairs = context.pairsByBackground[background.token] ?? [];
15998
+ const preferredPair = selectedBackground.token === background.token ? selectedPair ?? getPreferredPairForBackground(pairs) : getPreferredPairForBackground(pairs);
15999
+ const hasWhiteExample = supportsWhiteForegroundPreview(background);
16000
+ const isSelected = selectedBackground.token === background.token;
16001
+ 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`;
16002
+ return /* @__PURE__ */ jsx(
15771
16003
  "button",
15772
16004
  {
15773
16005
  type: "button",
15774
- onClick: () => handleBackgroundChange(color2.token),
16006
+ "aria-label": ariaLabel,
16007
+ "aria-pressed": isSelected,
16008
+ onClick: () => handleBackgroundChange(background.token),
15775
16009
  className: cn(
15776
16010
  "group relative h-12 rounded-sm border border-grey-200 transition-transform hover:scale-[1.03] dark:border-grey-700",
15777
16011
  isSelected && "ring-2 ring-primary-500 ring-offset-2 ring-offset-background"
15778
16012
  ),
15779
- style: { backgroundColor: color2.hex },
15780
- children: [
15781
- pair ? /* @__PURE__ */ jsx(
15782
- "span",
15783
- {
15784
- 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",
15785
- style: { backgroundColor: pair.foreground.hex }
15786
- }
15787
- ) : null,
15788
- /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
15789
- color2.token,
15790
- pair ? ` paired with ${pair.foreground.token}` : hasWhiteExample ? " has a white text example in this tool" : " has no recommended pair in this tool"
15791
- ] })
15792
- ]
16013
+ style: { backgroundColor: background.hex },
16014
+ children: preferredPair ? /* @__PURE__ */ jsx(
16015
+ "span",
16016
+ {
16017
+ 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",
16018
+ style: { backgroundColor: preferredPair.foreground.hex }
16019
+ }
16020
+ ) : null
15793
16021
  },
15794
- color2.token
16022
+ background.token
15795
16023
  );
15796
- }) }),
15797
- /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 text-sm text-muted-foreground", children: [
15798
- /* @__PURE__ */ jsxs("span", { children: [
15799
- "Selected background:",
15800
- " ",
15801
- /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: selectedBackground.token })
15802
- ] }),
15803
- /* @__PURE__ */ jsxs("span", { children: [
15804
- selectedPair ? "Recommended foreground:" : "White text example:",
15805
- " ",
15806
- /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: selectedPair?.foreground.token ?? whiteForegroundExample?.foreground.token ?? "None" })
15807
- ] })
16024
+ }) })
16025
+ ] }, group.key)),
16026
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-4 text-sm text-muted-foreground", children: [
16027
+ /* @__PURE__ */ jsxs("span", { children: [
16028
+ "Selected background:",
16029
+ " ",
16030
+ /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: selectedBackground.token })
16031
+ ] }),
16032
+ /* @__PURE__ */ jsxs("span", { children: [
16033
+ selectedPair ? "Selected foreground:" : "White text example:",
16034
+ " ",
16035
+ /* @__PURE__ */ jsx("strong", { className: "text-foreground", children: selectedPair?.foreground.token ?? whiteForegroundExample?.foreground.token ?? "None" })
15808
16036
  ] })
15809
16037
  ] })
15810
- ] }),
15811
- /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
15812
- /* @__PURE__ */ jsx(PairDetailCard, { color: selectedBackground, format, role: "Background" }),
15813
- detailForeground ? /* @__PURE__ */ jsx(PairDetailCard, { color: detailForeground, format, role: "Foreground" }) : /* @__PURE__ */ jsxs(Card, { className: "gap-4", children: [
15814
- /* @__PURE__ */ jsx(CardHeader, { className: "gap-3 border-b", children: /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
15815
- /* @__PURE__ */ jsx(CardTitle, { children: "Foreground" }),
15816
- /* @__PURE__ */ jsx(CardDescription, { children: "No recommended foreground available" })
15817
- ] }) }),
15818
- /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select another approved tone or review the recommended pairs list for the same family." }) })
15819
- ] })
15820
16038
  ] })
15821
16039
  ] }),
15822
- /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
15823
- /* @__PURE__ */ jsxs(CardHeader, { className: "gap-2 border-b", children: [
15824
- /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Recommended pairs" }),
15825
- /* @__PURE__ */ jsx(CardDescription, { children: "Recommended AAA combinations for this family, including white on eligible dark backgrounds." })
15826
- ] }),
15827
- /* @__PURE__ */ jsx(CardContent, { className: "grid gap-3", children: activeFamily.recommendedPairs.map((pair) => {
15828
- const isActive = selectedPair?.id === pair.id;
15829
- return /* @__PURE__ */ jsxs(
15830
- "button",
15831
- {
15832
- type: "button",
15833
- onClick: () => handlePairChange(pair.id),
15834
- className: cn(
15835
- "rounded-sm border p-4 text-left transition-colors",
15836
- 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"
15837
- ),
15838
- children: [
15839
- /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
15840
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
15841
- /* @__PURE__ */ jsx(
15842
- "div",
15843
- {
15844
- className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
15845
- style: { backgroundColor: pair.background.hex }
15846
- }
15847
- ),
15848
- /* @__PURE__ */ jsx(
15849
- "div",
15850
- {
15851
- className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
15852
- style: { backgroundColor: pair.foreground.hex }
15853
- }
15854
- )
15855
- ] }),
15856
- /* @__PURE__ */ 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: [
15857
- /* @__PURE__ */ jsx(Icons.contrast, { "data-slot": "icon", className: "size-4" }),
15858
- pair.rating
15859
- ] })
16040
+ /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
16041
+ /* @__PURE__ */ jsx(PairDetailCard, { color: selectedBackground, format, role: "Background" }),
16042
+ detailForeground ? /* @__PURE__ */ jsx(PairDetailCard, { color: detailForeground, format, role: "Foreground" }) : /* @__PURE__ */ jsxs(Card, { className: "gap-4", children: [
16043
+ /* @__PURE__ */ jsx(CardHeader, { className: "gap-3 border-b", children: /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
16044
+ /* @__PURE__ */ jsx(CardTitle, { children: "Foreground" }),
16045
+ /* @__PURE__ */ jsx(CardDescription, { children: "No recommended foreground available" })
16046
+ ] }) }),
16047
+ /* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Select another approved tone or review the recommended foregrounds for the same background." }) })
16048
+ ] })
16049
+ ] }),
16050
+ selectedPair ? /* @__PURE__ */ jsx(PairComplianceCard, { pair: selectedPair }) : null,
16051
+ showWhiteForegroundExample && whiteForegroundExample ? /* @__PURE__ */ jsx(WhiteTextExampleCard, { pair: whiteForegroundExample }) : null
16052
+ ] }),
16053
+ /* @__PURE__ */ jsx("div", { className: "space-y-6", children: /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
16054
+ /* @__PURE__ */ jsxs(CardHeader, { className: "gap-2 border-b", children: [
16055
+ /* @__PURE__ */ jsx(CardTitle, { className: "text-base", children: "Recommended foregrounds" }),
16056
+ /* @__PURE__ */ jsxs(CardDescription, { children: [
16057
+ "AAA combinations for ",
16058
+ selectedBackground.token,
16059
+ ", drawn from",
16060
+ " ",
16061
+ context.primary.label,
16062
+ ", ",
16063
+ context.accent.label,
16064
+ ", Grey, and white using only foreground tones 50, 200, 400, 600, and 800."
16065
+ ] })
16066
+ ] }),
16067
+ /* @__PURE__ */ jsx(CardContent, { className: "grid gap-3", children: selectedBackgroundPairs.length > 0 ? selectedBackgroundPairs.map((pair) => {
16068
+ const isActive = selectedPair?.id === pair.id;
16069
+ return /* @__PURE__ */ jsxs(
16070
+ "button",
16071
+ {
16072
+ type: "button",
16073
+ "aria-label": `Use ${pair.foreground.token} on ${pair.background.token}`,
16074
+ "aria-pressed": isActive,
16075
+ onClick: () => handlePairChange(pair.id),
16076
+ className: cn(
16077
+ "rounded-sm border p-4 text-left transition-colors",
16078
+ 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"
16079
+ ),
16080
+ children: [
16081
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
16082
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
16083
+ /* @__PURE__ */ jsx(
16084
+ "div",
16085
+ {
16086
+ className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
16087
+ style: { backgroundColor: pair.background.hex }
16088
+ }
16089
+ ),
16090
+ /* @__PURE__ */ jsx(
16091
+ "div",
16092
+ {
16093
+ className: "size-11 rounded-sm border border-grey-200 shadow-sm dark:border-grey-700",
16094
+ style: { backgroundColor: pair.foreground.hex }
16095
+ }
16096
+ )
15860
16097
  ] }),
15861
- /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-1", children: [
15862
- /* @__PURE__ */ jsxs("p", { className: "font-medium text-foreground", children: [
15863
- pair.background.token,
15864
- " / ",
15865
- pair.foreground.token
15866
- ] }),
15867
- /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
15868
- pair.contrastRatio.toFixed(2),
15869
- ":1 contrast ratio"
15870
- ] })
16098
+ /* @__PURE__ */ 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: [
16099
+ /* @__PURE__ */ jsx(Icons.contrast, { "data-slot": "icon", className: "size-4" }),
16100
+ pair.rating
15871
16101
  ] })
15872
- ]
15873
- },
15874
- pair.id
15875
- );
15876
- }) })
15877
- ] }) })
15878
- ] }),
15879
- selectedPair ? /* @__PURE__ */ jsx(PairComplianceCard, { pair: selectedPair }) : null,
15880
- showWhiteForegroundExample && whiteForegroundExample ? /* @__PURE__ */ jsx(WhiteTextExampleCard, { pair: whiteForegroundExample }) : null
15881
- ] })
16102
+ ] }),
16103
+ /* @__PURE__ */ jsxs("div", { className: "mt-4 space-y-1", children: [
16104
+ /* @__PURE__ */ jsxs("p", { className: "font-medium text-foreground", children: [
16105
+ pair.background.token,
16106
+ " / ",
16107
+ pair.foreground.token
16108
+ ] }),
16109
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
16110
+ pair.foreground.familyLabel,
16111
+ " on ",
16112
+ pair.background.familyLabel
16113
+ ] }),
16114
+ /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", children: [
16115
+ pair.contrastRatio.toFixed(2),
16116
+ ":1 contrast ratio"
16117
+ ] })
16118
+ ] })
16119
+ ]
16120
+ },
16121
+ pair.id
16122
+ );
16123
+ }) : /* @__PURE__ */ 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." }) })
16124
+ ] }) })
16125
+ ] }) })
15882
16126
  ] });
15883
16127
  }
15884
16128
  function ColorPairingTool() {
@@ -18366,7 +18610,7 @@ function FormMessage({ className, ...props }) {
18366
18610
 
18367
18611
  // package.json
18368
18612
  var package_default = {
18369
- version: "1.98.0"};
18613
+ version: "1.99.0"};
18370
18614
  var SluggerContext = React5__default.createContext(null);
18371
18615
  function flattenText(nodes) {
18372
18616
  if (nodes == null || typeof nodes === "boolean") return "";