pixelize-design-library 2.4.2-beta.27 → 2.4.2-beta.29

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.
Files changed (38) hide show
  1. package/dist/Components/Button/Button.styles.js +22 -2
  2. package/dist/Components/DatePicker/CalendarPanel.d.ts +5 -0
  3. package/dist/Components/DatePicker/CalendarPanel.js +23 -4
  4. package/dist/Components/DatePicker/DatePickerProps.d.ts +21 -0
  5. package/dist/Components/DatePicker/RangeDatePicker.js +2 -2
  6. package/dist/Components/DatePicker/dayStates.d.ts +56 -0
  7. package/dist/Components/DatePicker/dayStates.js +87 -0
  8. package/dist/Theme/assertPalette.d.ts +1 -1
  9. package/dist/Theme/assertPalette.js +3 -2
  10. package/dist/Theme/chakra/createBrandTheme.js +7 -0
  11. package/dist/Theme/chakra/focusRing.styles.d.ts +52 -0
  12. package/dist/Theme/chakra/focusRing.styles.js +46 -0
  13. package/dist/Theme/tokens/brands/index.js +6 -1
  14. package/dist/Theme/tokens/brands/zinc/palette.dark.d.ts +1 -0
  15. package/dist/Theme/tokens/builders/buildDarkPalette.js +6 -1
  16. package/dist/Theme/tokens/builders/focusRing.d.ts +52 -0
  17. package/dist/Theme/tokens/builders/focusRing.js +92 -0
  18. package/dist/Theme/tokens/builders/index.d.ts +1 -0
  19. package/dist/Theme/tokens/builders/index.js +1 -0
  20. package/dist/Theme/tokens/builders/outlineRung.d.ts +8 -2
  21. package/dist/Theme/tokens/builders/outlineRung.js +8 -2
  22. package/dist/Theme/tokens/types.d.ts +21 -1
  23. package/dist/esm/Components/Button/Button.styles.js +22 -2
  24. package/dist/esm/Components/DatePicker/CalendarPanel.js +23 -4
  25. package/dist/esm/Components/DatePicker/RangeDatePicker.js +2 -2
  26. package/dist/esm/Components/DatePicker/dayStates.js +79 -0
  27. package/dist/esm/Theme/assertPalette.js +3 -2
  28. package/dist/esm/Theme/chakra/createBrandTheme.js +7 -0
  29. package/dist/esm/Theme/chakra/focusRing.styles.js +40 -0
  30. package/dist/esm/Theme/tokens/brands/index.js +6 -1
  31. package/dist/esm/Theme/tokens/builders/buildDarkPalette.js +6 -1
  32. package/dist/esm/Theme/tokens/builders/focusRing.js +85 -0
  33. package/dist/esm/Theme/tokens/builders/index.js +1 -0
  34. package/dist/esm/Theme/tokens/builders/outlineRung.js +8 -2
  35. package/dist/esm/index.js +10 -0
  36. package/dist/index.d.ts +6 -1
  37. package/dist/index.js +26 -2
  38. package/package.json +1 -1
@@ -4,6 +4,7 @@ exports.Button = void 0;
4
4
  const accentText_1 = require("../../Theme/tokens/builders/accentText");
5
5
  const color_1 = require("../../Theme/tokens/builders/color");
6
6
  const gradientInk_1 = require("../../Theme/tokens/builders/gradientInk");
7
+ const focusRing_styles_1 = require("../../Theme/chakra/focusRing.styles");
7
8
  /**
8
9
  * Outline/ghost/link text sits ON the canvas, so it needs a rung that stays readable there —
9
10
  * `[500]` is pinned dark enough to carry a white label on the solid variant and measures only
@@ -62,8 +63,19 @@ const darkerFill = (theme, colorScheme, step) => {
62
63
  };
63
64
  /** Label for the solid variant's filled bg — derived from THAT fill, since hover/active swap it. */
64
65
  const labelFor = (theme, fill) => (0, accentText_1.onFilled)(fill, theme.colors.white, theme.colors.black);
66
+ /**
67
+ * The two-tone `:focus-visible` ring, from the palette's derived `focusRing` pair.
68
+ *
69
+ * Every variant spreads this. Before it, none of them defined `_focusVisible` at all, so every
70
+ * button in every consuming app fell through to Chakra's stock `shadows.outline` — one fixed blue
71
+ * that measured 1.83:1 on the canvas and 1.38:1 on a primary fill, failing WCAG 1.4.11 (3:1) on
72
+ * all nine brands. `focusRing.test.ts` is the gate that keeps this true.
73
+ */
74
+ const focusRing = (theme) => (0, focusRing_styles_1.focusVisibleStyles)(theme.colors);
65
75
  exports.Button = {
66
- baseStyle: () => ({
76
+ // The ring is repeated on every variant below AND here, so a variant name this file does not
77
+ // define (a consumer's own, or an `IconButton` falling through) still gets an indicator.
78
+ baseStyle: ({ theme }) => ({
67
79
  borderRadius: "lg",
68
80
  fontWeight: 500,
69
81
  fontSize: "1rem",
@@ -72,6 +84,7 @@ exports.Button = {
72
84
  opacity: 0.6,
73
85
  cursor: "not-allowed",
74
86
  },
87
+ ...focusRing(theme),
75
88
  }),
76
89
  variants: {
77
90
  solid: ({ theme, colorScheme = "primary" }) => {
@@ -104,6 +117,7 @@ exports.Button = {
104
117
  color: hoverLabel,
105
118
  },
106
119
  },
120
+ ...focusRing(theme),
107
121
  };
108
122
  },
109
123
  outline: ({ theme, colorScheme = "primary" }) => {
@@ -133,6 +147,7 @@ exports.Button = {
133
147
  // bg: theme.colors[colorScheme][500],
134
148
  color: ink,
135
149
  },
150
+ ...focusRing(theme),
136
151
  };
137
152
  },
138
153
  ghost: ({ theme, colorScheme = "primary" }) => ({
@@ -145,6 +160,7 @@ exports.Button = {
145
160
  _active: {
146
161
  bg: onCanvasHoverBg(theme, colorScheme, 24),
147
162
  },
163
+ ...focusRing(theme),
148
164
  }),
149
165
  link: ({ theme, colorScheme = "primary" }) => ({
150
166
  color: onCanvasText(theme, colorScheme),
@@ -157,13 +173,16 @@ exports.Button = {
157
173
  _active: {
158
174
  color: onCanvasText(theme, colorScheme),
159
175
  },
176
+ ...focusRing(theme),
160
177
  }),
161
- unstyled: () => ({
178
+ // `unstyled` drops the fill, not the affordance: a focus indicator is not styling.
179
+ unstyled: ({ theme }) => ({
162
180
  bg: "transparent",
163
181
  color: "inherit",
164
182
  _hover: {
165
183
  bg: "transparent",
166
184
  },
185
+ ...focusRing(theme),
167
186
  }),
168
187
  // Theme-based two-tone gradient: each theme defines its own `gradient` {from,to} pair.
169
188
  // Ignores colorScheme; hover/active shift brightness so it works for any pair.
@@ -190,6 +209,7 @@ exports.Button = {
190
209
  filter: "brightness(0.92)",
191
210
  _hover: { bgImage: bg, filter: "brightness(0.92)" },
192
211
  },
212
+ ...focusRing(theme),
193
213
  };
194
214
  },
195
215
  },
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { type DayStates, type RenderDayContent } from "./dayStates";
2
3
  export type CalendarPanelProps = {
3
4
  currentMonth: Date;
4
5
  setCurrentMonth: (updater: (prev: Date) => Date) => void;
@@ -19,6 +20,10 @@ export type CalendarPanelProps = {
19
20
  hoveredDate?: Date | null;
20
21
  onDayHover?: (d: Date | null) => void;
21
22
  renderWeekdays?: React.ReactNode;
23
+ /** Marks individual days (weekly off, holiday, …). See `dayStates.ts`. Omit for the plain grid. */
24
+ dayStates?: DayStates;
25
+ /** Custom content inside a day cell, replacing that day's built-in marker dot. */
26
+ renderDayContent?: RenderDayContent;
22
27
  };
23
28
  export declare const CalendarPanel: React.FC<CalendarPanelProps>;
24
29
  export default CalendarPanel;
@@ -11,7 +11,8 @@ const lucide_react_1 = require("lucide-react");
11
11
  const Button_1 = __importDefault(require("../Button/Button"));
12
12
  const useCustomTheme_1 = require("../../Theme/useCustomTheme");
13
13
  const accentText_1 = require("../../Theme/tokens/builders/accentText");
14
- const CalendarPanel = ({ currentMonth, setCurrentMonth, today, isRange, tempDate, tempRangeStart, tempRangeEnd, isSameDay, isBefore, isAfter, minDate, maxDate, disablePastDates, disableFutureDates, disableToday, onDaySelect, hoveredDate, onDayHover, renderWeekdays, }) => {
14
+ const dayStates_1 = require("./dayStates");
15
+ const CalendarPanel = ({ currentMonth, setCurrentMonth, today, isRange, tempDate, tempRangeStart, tempRangeEnd, isSameDay, isBefore, isAfter, minDate, maxDate, disablePastDates, disableFutureDates, disableToday, onDaySelect, hoveredDate, onDayHover, renderWeekdays, dayStates, renderDayContent, }) => {
15
16
  const theme = (0, useCustomTheme_1.useCustomTheme)();
16
17
  const rangeBg = theme.colors.primary.opacity[16];
17
18
  const isViewingCurrentMonth = currentMonth.getFullYear() === today.getFullYear() &&
@@ -51,6 +52,7 @@ const CalendarPanel = ({ currentMonth, setCurrentMonth, today, isRange, tempDate
51
52
  },
52
53
  };
53
54
  const renderDays = () => {
55
+ var _a;
54
56
  const monthStart = (0, date_fns_1.startOfMonth)(currentMonth);
55
57
  const monthEnd = (0, date_fns_1.endOfMonth)(currentMonth);
56
58
  const startDate = (0, date_fns_1.startOfWeek)(monthStart);
@@ -69,12 +71,17 @@ const CalendarPanel = ({ currentMonth, setCurrentMonth, today, isRange, tempDate
69
71
  const isSelected = !!(!isRange && tempDate && isSameDay(thisDay, tempDate));
70
72
  const isTentativeEnd = isEnd && isTentative;
71
73
  const circleSolid = !!(isSelected || (isRange && isStart) || (isEnd && !isTentative));
74
+ // Marking is not disabling: only the `disabled` state key blocks selection, and only
75
+ // because the caller asked for it by name.
76
+ const states = (_a = dayStates === null || dayStates === void 0 ? void 0 : dayStates(thisDay)) !== null && _a !== void 0 ? _a : [];
77
+ const stateVisual = states.length ? (0, dayStates_1.resolveDayStateVisual)(states, theme.colors) : null;
72
78
  const disabled = !isInCurrentMonth ||
73
79
  !!(minDate && isBefore(thisDay, minDate)) ||
74
80
  !!(maxDate && isAfter(thisDay, maxDate)) ||
75
81
  !!(disablePastDates && isBefore(thisDay, today) && !isSameDay(thisDay, today)) ||
76
82
  !!(disableFutureDates && isAfter(thisDay, today) && !isSameDay(thisDay, today)) ||
77
- !!(disableToday && isToday);
83
+ !!(disableToday && isToday) ||
84
+ !!(stateVisual === null || stateVisual === void 0 ? void 0 : stateVisual.blocksSelection);
78
85
  // Connected range band painted on the cell background; start/end use a
79
86
  // half gradient so the band meets the solid day circle cleanly.
80
87
  let cellBg;
@@ -88,6 +95,9 @@ const CalendarPanel = ({ currentMonth, setCurrentMonth, today, isRange, tempDate
88
95
  else if (isEnd)
89
96
  cellBg = `linear-gradient(to right, ${rangeBg} 50%, transparent 50%)`;
90
97
  }
98
+ // Selection feedback outranks a state tint — the band is what tells the user what they picked.
99
+ if (!cellBg && (stateVisual === null || stateVisual === void 0 ? void 0 : stateVisual.cellBg))
100
+ cellBg = stateVisual.cellBg;
91
101
  // Solid fill (selected/range-endpoint) needs the ink checked against that
92
102
  // specific fill — `primary[500]` doesn't reliably clear AA for white text on
93
103
  // every brand/mode (same gap the solid Button variant already accounts for).
@@ -108,11 +118,20 @@ const CalendarPanel = ({ currentMonth, setCurrentMonth, today, isRange, tempDate
108
118
  : isToday
109
119
  ? `1px solid ${theme.colors.primary[300]}`
110
120
  : "1px solid transparent";
111
- days.push((0, jsx_runtime_1.jsx)(react_1.Box, { h: "2rem", display: "flex", alignItems: "center", justifyContent: "center", style: cellBg ? { background: cellBg } : undefined, children: (0, jsx_runtime_1.jsx)(react_1.Box, { as: "button", type: "button", disabled: disabled, onClick: () => !disabled && onDaySelect(new Date(thisDay)), onMouseEnter: () => !disabled && (onDayHover === null || onDayHover === void 0 ? void 0 : onDayHover(new Date(thisDay))), w: "1.75rem", h: "1.75rem", borderRadius: "full", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.8125rem", fontWeight: circleSolid ? 600 : 500, lineHeight: "1", cursor: disabled ? "not-allowed" : "pointer", bg: circleSolid ? solidFill : "transparent", color: circleColor, border: circleBorder, transition: "background 0.12s, color 0.12s", _hover: disabled
121
+ // A filled circle owns its own ink (checked against that fill by `onFilled`), so a state
122
+ // never repaints a selected day — it would undo that contrast guarantee.
123
+ const dayColor = !circleSolid && (stateVisual === null || stateVisual === void 0 ? void 0 : stateVisual.color) ? stateVisual.color : circleColor;
124
+ const dayBorder = !circleSolid && (stateVisual === null || stateVisual === void 0 ? void 0 : stateVisual.border) ? stateVisual.border : circleBorder;
125
+ const customContent = renderDayContent ? renderDayContent(thisDay, states) : null;
126
+ const markerColor = customContent == null && (stateVisual === null || stateVisual === void 0 ? void 0 : stateVisual.markerColor) ? stateVisual.markerColor : null;
127
+ const hasDayOverlay = customContent != null || markerColor != null;
128
+ days.push((0, jsx_runtime_1.jsx)(react_1.Box, { h: "2rem", display: "flex", alignItems: "center", justifyContent: "center", style: cellBg ? { background: cellBg } : undefined, children: (0, jsx_runtime_1.jsxs)(react_1.Box, { as: "button", type: "button", disabled: disabled, className: dayStates ? (0, dayStates_1.dayStateClassName)(states) : undefined, "aria-label": dayStates ? (0, dayStates_1.dayAccessibleName)((0, date_fns_1.format)(thisDay, "d MMMM yyyy"), states) : undefined, onClick: () => !disabled && onDaySelect(new Date(thisDay)), onMouseEnter: () => !disabled && (onDayHover === null || onDayHover === void 0 ? void 0 : onDayHover(new Date(thisDay))), position: hasDayOverlay ? "relative" : undefined, w: "1.75rem", h: "1.75rem", borderRadius: "full", display: "flex", alignItems: "center", justifyContent: "center", fontSize: "0.8125rem", fontWeight: circleSolid ? 600 : 500, lineHeight: "1", cursor: disabled ? "not-allowed" : "pointer", bg: circleSolid ? solidFill : "transparent", color: dayColor, textDecoration: (stateVisual === null || stateVisual === void 0 ? void 0 : stateVisual.strikethrough) ? "line-through" : undefined, border: dayBorder, transition: "background 0.12s, color 0.12s", _hover: disabled
112
129
  ? {}
113
130
  : circleSolid
114
131
  ? { bg: solidHoverFill, color: circleHoverColor }
115
- : { bg: rangeBg }, children: (0, date_fns_1.format)(thisDay, "d") }) }, thisDay.toString()));
132
+ : { bg: rangeBg }, children: [(0, date_fns_1.format)(thisDay, "d"), hasDayOverlay && ((0, jsx_runtime_1.jsx)(react_1.Box, { as: "span", className: `${dayStates_1.DAY_CLASS}__content`, position: "absolute", left: 0, right: 0, bottom: "0.0625rem", display: "flex", alignItems: "flex-end", justifyContent: "center", lineHeight: "1", fontSize: "0.5rem",
133
+ // The overlay is decoration: clicks belong to the day button under it.
134
+ pointerEvents: "none", children: customContent !== null && customContent !== void 0 ? customContent : ((0, jsx_runtime_1.jsx)(react_1.Box, { as: "span", className: `${dayStates_1.DAY_CLASS}__marker`, w: "0.25rem", h: "0.25rem", borderRadius: "full", bg: markerColor })) }))] }) }, thisDay.toString()));
116
135
  day = (0, date_fns_1.addDays)(day, 1);
117
136
  }
118
137
  rows.push((0, jsx_runtime_1.jsx)(react_1.Grid, { templateColumns: "repeat(7, 1fr)", gap: 0, children: days }, day.toString()));
@@ -1,3 +1,4 @@
1
+ import type { DayStates, RenderDayContent } from "./dayStates";
1
2
  export type PickerType = "date" | "time" | "datetime";
2
3
  export type BaseDatePickerProps = {
3
4
  id?: string;
@@ -40,6 +41,26 @@ export type RangeDatePickerProps = BaseDatePickerProps & {
40
41
  /** Alias for `selectedDate` (legacy / form libraries). */
41
42
  value?: RangeDate | null;
42
43
  onChange: (range: RangeDate | null) => void;
44
+ /**
45
+ * Marks individual days with zero or more state keys — `["weekly-off"]`, `["Onam", "holiday"]`,
46
+ * `["excluded"]`. The keys in `DAY_STATE_KEYS` (`weekly-off`, `holiday`, `optional-holiday`,
47
+ * `excluded`, `disabled`) carry built-in, token-driven styling; any other key is announced to a
48
+ * screen reader but left unstyled, which is how a day gets named without a second prop.
49
+ *
50
+ * Marking is not disabling: a marked day stays selectable unless the caller also returns
51
+ * `"disabled"`, or blocks the day with `minDate`/`maxDate`/`disablePastDates`/`disableFutureDates`.
52
+ *
53
+ * Supplying this also spells each day's accessible name out in full — "16 September 2025, Onam,
54
+ * holiday" instead of "16". Called once per rendered day (42 per month), so keep it cheap and pure.
55
+ */
56
+ dayStates?: DayStates;
57
+ /**
58
+ * Custom content rendered inside the day cell, centred under the day number — a dot, a short
59
+ * label, a tooltip trigger. Receives that day's resolved state keys. Returning a node replaces
60
+ * the built-in marker dot for that day; the number, the accessible name and hit target are
61
+ * unchanged (the overlay does not take pointer events).
62
+ */
63
+ renderDayContent?: RenderDayContent;
43
64
  };
44
65
  export type DatePickerProps = SingleDatePickerProps | RangeDatePickerProps;
45
66
  export type TimePickerProps = {
@@ -25,7 +25,7 @@ const setTimeFrom = (base, time) => {
25
25
  };
26
26
  const RangeDatePicker = (props) => {
27
27
  var _a, _b, _c, _d;
28
- const { id, name, label, isRequired, isInformation, informationMessage, error, errorMessage, helperText, placeholderText, dateFormat = "dd/MM/yyyy", minDate, maxDate, disableFutureDates, disablePastDates, autoComplete = "off", disabled, width = "100%", disableToday = false, size = "md", } = props;
28
+ const { id, name, label, isRequired, isInformation, informationMessage, error, errorMessage, helperText, placeholderText, dateFormat = "dd/MM/yyyy", minDate, maxDate, disableFutureDates, disablePastDates, autoComplete = "off", disabled, width = "100%", disableToday = false, size = "md", dayStates, renderDayContent, } = props;
29
29
  const theme = (0, useCustomTheme_1.useCustomTheme)();
30
30
  const sizeKey = typeof size === "string" ? size : "md";
31
31
  const calendarIconPx = (0, textInputIconSize_1.getTextInputIconSizePx)(sizeKey);
@@ -181,7 +181,7 @@ const RangeDatePicker = (props) => {
181
181
  }, cursor: "pointer" }), (0, jsx_runtime_1.jsx)(react_2.InputRightElement, { height: "100%", display: "flex", alignItems: "center", pointerEvents: "auto", style: { cursor: "pointer" }, onClick: () => (isOpen ? onClose() : handleOpen()), children: (0, jsx_runtime_1.jsx)(react_2.Box, { as: "span", sx: calendarIconWrapperSx, children: (0, jsx_runtime_1.jsx)(lucide_react_1.CalendarIcon, { size: calendarIconPx, color: theme.colors.textMuted }) }) })] }) }) }), (0, jsx_runtime_1.jsx)(react_2.PopoverContent, { w: "18rem", maxW: "18rem", p: 3, ref: popoverRef, boxShadow: "lg", borderRadius: "xl", bg: theme.colors.background[50], children: (0, jsx_runtime_1.jsxs)(react_2.PopoverBody, { p: 0, children: [(0, jsx_runtime_1.jsx)(react_2.Box, { display: "flex", flexWrap: "wrap", gap: 2, mb: 3, children: presets.map((p) => ((0, jsx_runtime_1.jsx)(Button_1.default, { size: "xs", variant: "outline", colorScheme: "gray", sx: { flex: "1 1 auto" }, onClick: () => {
182
182
  const [from, to] = p.getRange();
183
183
  applyPreset(from, to);
184
- }, children: p.label }, p.label))) }), (0, jsx_runtime_1.jsx)(CalendarPanel_1.default, { currentMonth: currentMonth, setCurrentMonth: (updater) => setCurrentMonthState((prev) => updater(prev)), today: today, isRange: true, tempDate: null, tempRangeStart: tempRangeStart, tempRangeEnd: tempRangeEnd, hoveredDate: hoveredDate, onDayHover: setHoveredDate, isSameDay: date_fns_1.isSameDay, isBefore: date_fns_1.isBefore, isAfter: date_fns_1.isAfter, minDate: minDate, maxDate: maxDate, disablePastDates: disablePastDates, disableFutureDates: disableFutureDates, disableToday: disableToday, onDaySelect: handleDaySelect }), formatHasTime && ((0, jsx_runtime_1.jsxs)(react_2.Box, { mt: 3, display: "grid", gridTemplateColumns: "1fr", gap: 2, children: [(0, jsx_runtime_1.jsxs)(react_2.Box, { children: [(0, jsx_runtime_1.jsx)(react_2.Box, { fontSize: "xs", color: theme.colors.textMuted, mb: 1, children: "Start time" }), shouldUseSelectTimePicker ? ((0, jsx_runtime_1.jsx)(TimePicker_1.default, { date: startTimeBaseDate, onChange: (updated) => {
184
+ }, children: p.label }, p.label))) }), (0, jsx_runtime_1.jsx)(CalendarPanel_1.default, { currentMonth: currentMonth, setCurrentMonth: (updater) => setCurrentMonthState((prev) => updater(prev)), today: today, isRange: true, tempDate: null, tempRangeStart: tempRangeStart, tempRangeEnd: tempRangeEnd, hoveredDate: hoveredDate, onDayHover: setHoveredDate, isSameDay: date_fns_1.isSameDay, isBefore: date_fns_1.isBefore, isAfter: date_fns_1.isAfter, minDate: minDate, maxDate: maxDate, disablePastDates: disablePastDates, disableFutureDates: disableFutureDates, disableToday: disableToday, onDaySelect: handleDaySelect, dayStates: dayStates, renderDayContent: renderDayContent }), formatHasTime && ((0, jsx_runtime_1.jsxs)(react_2.Box, { mt: 3, display: "grid", gridTemplateColumns: "1fr", gap: 2, children: [(0, jsx_runtime_1.jsxs)(react_2.Box, { children: [(0, jsx_runtime_1.jsx)(react_2.Box, { fontSize: "xs", color: theme.colors.textMuted, mb: 1, children: "Start time" }), shouldUseSelectTimePicker ? ((0, jsx_runtime_1.jsx)(TimePicker_1.default, { date: startTimeBaseDate, onChange: (updated) => {
185
185
  if (!tempRangeStart) {
186
186
  setTempRangeStart(updated);
187
187
  return;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Day-state marking for the calendar grid.
3
+ *
4
+ * A *state* is a key describing what a single day MEANS in the caller's domain — a weekly off,
5
+ * a public holiday, a day excluded from a leave count. It is deliberately separate from
6
+ * disabling: a marked day stays selectable unless the caller also returns the `"disabled"` key
7
+ * (or blocks the day through `minDate`/`maxDate`/`disablePastDates`/`disableFutureDates`).
8
+ *
9
+ * Every key returned reaches a screen reader. Only the keys in `DAY_STATE_KEYS` also carry
10
+ * built-in styling, so a caller can return a free-text key ("Onam") purely to have it announced
11
+ * alongside the recognised one: `["Onam", "holiday"]` → "16 September 2025, Onam, holiday".
12
+ *
13
+ * Colour comes from the palette only — every visual below is a function of `PaletteProps`, so a
14
+ * marked day follows the active light/dark palette like the rest of `CalendarPanel`.
15
+ */
16
+ import type { ReactNode } from "react";
17
+ import type { PaletteProps } from "../../Theme/tokens/types";
18
+ /**
19
+ * The keys with built-in styling, in ascending precedence — when a day carries several, a later
20
+ * key's visual overrides the fields an earlier one set.
21
+ */
22
+ export declare const DAY_STATE_KEYS: readonly ["weekly-off", "holiday", "optional-holiday", "excluded", "disabled"];
23
+ export type DayStateKey = (typeof DAY_STATE_KEYS)[number];
24
+ /** Returns zero or more state keys for a day. Unrecognised keys are announced, never styled. */
25
+ export type DayStates = (date: Date) => string[] | undefined;
26
+ /** Custom content rendered inside a day cell, beneath the day number. */
27
+ export type RenderDayContent = (date: Date, states: string[]) => ReactNode;
28
+ /** What a resolved set of states paints. Every field is optional — an absent field changes nothing. */
29
+ export type DayStateVisual = {
30
+ /** Tint painted across the whole cell. The range band always wins over it. */
31
+ cellBg?: string;
32
+ /** Ink for the day number. Never applied to a selected/endpoint day, whose fill owns its ink. */
33
+ color?: string;
34
+ /** `border` shorthand for the day circle, replacing the today/tentative ring. */
35
+ border?: string;
36
+ /** Fill of the small marker dot under the number. `renderDayContent` replaces it. */
37
+ markerColor?: string;
38
+ /** Strikes the day number through — the "counted, but not really" states. */
39
+ strikethrough?: boolean;
40
+ /** The one state that also blocks selection. Marking alone never does. */
41
+ blocksSelection?: boolean;
42
+ };
43
+ export declare const isDayStateKey: (key: string) => key is DayStateKey;
44
+ /** Merges every recognised state in `DAY_STATE_KEYS` order, so precedence never depends on caller order. */
45
+ export declare const resolveDayStateVisual: (states: string[], colors: PaletteProps) => DayStateVisual;
46
+ /** Base class on every day button once `dayStates` is supplied. Consumers may style off it. */
47
+ export declare const DAY_CLASS = "pxl-calendar-day";
48
+ /** `["Onam", "holiday"]` → `"pxl-calendar-day pxl-calendar-day--onam pxl-calendar-day--holiday"`. */
49
+ export declare const dayStateClassName: (states: string[]) => string;
50
+ /** Human phrasing for the accessible name — every key, styled or not, in the caller's own order. */
51
+ export declare const describeDayStates: (states: string[]) => string;
52
+ /**
53
+ * The day's accessible name. The date is spelled out because "16" alone tells a screen-reader
54
+ * user nothing, and the states follow it so the marking is heard, not only seen.
55
+ */
56
+ export declare const dayAccessibleName: (formattedDate: string, states: string[]) => string;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dayAccessibleName = exports.describeDayStates = exports.dayStateClassName = exports.DAY_CLASS = exports.resolveDayStateVisual = exports.isDayStateKey = exports.DAY_STATE_KEYS = void 0;
4
+ /**
5
+ * The keys with built-in styling, in ascending precedence — when a day carries several, a later
6
+ * key's visual overrides the fields an earlier one set.
7
+ */
8
+ exports.DAY_STATE_KEYS = [
9
+ "weekly-off",
10
+ "holiday",
11
+ "optional-holiday",
12
+ "excluded",
13
+ "disabled",
14
+ ];
15
+ /**
16
+ * Token roles, not rungs (see `Theme/THEME-CONTRACT.md`):
17
+ * - a non-working day is a *surface* step down plus muted ink — `backgroundColor.muted` / `textMuted`
18
+ * - a holiday is *status ink*, so `semanticText.<tone>`, never the `semantic.<tone>[500]` fill rung
19
+ * - an optional holiday is the same tone family one step less certain: warning + a dashed ring
20
+ * - an unavailable day is the `disabled` ramp, the only ramp whose job is exactly this
21
+ */
22
+ const VISUALS = {
23
+ "weekly-off": (colors) => ({
24
+ cellBg: colors.backgroundColor.muted,
25
+ color: colors.textMuted,
26
+ }),
27
+ holiday: (colors) => ({
28
+ color: colors.semanticText.info,
29
+ markerColor: colors.semanticText.info,
30
+ }),
31
+ "optional-holiday": (colors) => ({
32
+ color: colors.semanticText.warning,
33
+ markerColor: colors.semanticText.warning,
34
+ border: `1px dashed ${colors.semanticText.warning}`,
35
+ }),
36
+ excluded: (colors) => ({
37
+ color: colors.textMuted,
38
+ strikethrough: true,
39
+ }),
40
+ disabled: (colors) => ({
41
+ color: colors.disabled[500],
42
+ blocksSelection: true,
43
+ }),
44
+ };
45
+ const isDayStateKey = (key) => exports.DAY_STATE_KEYS.includes(key);
46
+ exports.isDayStateKey = isDayStateKey;
47
+ /** Merges every recognised state in `DAY_STATE_KEYS` order, so precedence never depends on caller order. */
48
+ const resolveDayStateVisual = (states, colors) => {
49
+ const visual = {};
50
+ for (const key of exports.DAY_STATE_KEYS) {
51
+ if (states.includes(key))
52
+ Object.assign(visual, VISUALS[key](colors));
53
+ }
54
+ return visual;
55
+ };
56
+ exports.resolveDayStateVisual = resolveDayStateVisual;
57
+ /** Base class on every day button once `dayStates` is supplied. Consumers may style off it. */
58
+ exports.DAY_CLASS = "pxl-calendar-day";
59
+ const slug = (key) => key
60
+ .trim()
61
+ .toLowerCase()
62
+ .replace(/[^a-z0-9]+/g, "-")
63
+ .replace(/^-+|-+$/g, "");
64
+ /** `["Onam", "holiday"]` → `"pxl-calendar-day pxl-calendar-day--onam pxl-calendar-day--holiday"`. */
65
+ const dayStateClassName = (states) => [
66
+ exports.DAY_CLASS,
67
+ ...states
68
+ .map(slug)
69
+ .filter(Boolean)
70
+ .map((key) => `${exports.DAY_CLASS}--${key}`),
71
+ ].join(" ");
72
+ exports.dayStateClassName = dayStateClassName;
73
+ /** Human phrasing for the accessible name — every key, styled or not, in the caller's own order. */
74
+ const describeDayStates = (states) => states
75
+ .map((state) => ((0, exports.isDayStateKey)(state) ? state.replace(/-/g, " ") : state.trim()))
76
+ .filter(Boolean)
77
+ .join(", ");
78
+ exports.describeDayStates = describeDayStates;
79
+ /**
80
+ * The day's accessible name. The date is spelled out because "16" alone tells a screen-reader
81
+ * user nothing, and the states follow it so the marking is heard, not only seen.
82
+ */
83
+ const dayAccessibleName = (formattedDate, states) => {
84
+ const described = (0, exports.describeDayStates)(states);
85
+ return described ? `${formattedDate}, ${described}` : formattedDate;
86
+ };
87
+ exports.dayAccessibleName = dayAccessibleName;
@@ -1,5 +1,5 @@
1
1
  import type { PaletteProps } from "./tokens/types";
2
- declare const REQUIRED_GROUPS: readonly ["gradient", "primary", "secondary", "tertiary", "transparent", "black", "white", "semantic", "gray", "red", "orange", "yellow", "green", "teal", "blue", "cyan", "purple", "pink", "backgroundColor", "background", "text", "header", "placeholder", "boxShadow", "sidebar", "boxborder", "border", "table", "disabled", "accentText", "textMuted", "semanticText", "onPrimary", "onSemantic"];
2
+ declare const REQUIRED_GROUPS: readonly ["gradient", "primary", "secondary", "tertiary", "transparent", "black", "white", "semantic", "gray", "red", "orange", "yellow", "green", "teal", "blue", "cyan", "purple", "pink", "backgroundColor", "background", "text", "header", "placeholder", "boxShadow", "focusRing", "sidebar", "boxborder", "border", "table", "disabled", "accentText", "textMuted", "semanticText", "onPrimary", "onSemantic"];
3
3
  type Unlisted = Exclude<keyof PaletteProps, (typeof REQUIRED_GROUPS)[number]>;
4
4
  export type AllPaletteTokensListed = Unlisted extends never ? true : ["unlisted token", Unlisted];
5
5
  export declare const allPaletteTokensListed: AllPaletteTokensListed;
@@ -5,8 +5,8 @@ const REQUIRED_GROUPS = [
5
5
  "gradient", "primary", "secondary", "tertiary", "transparent", "black", "white",
6
6
  "semantic", "gray", "red", "orange", "yellow", "green", "teal", "blue", "cyan",
7
7
  "purple", "pink", "backgroundColor", "background", "text", "header", "placeholder",
8
- "boxShadow", "sidebar", "boxborder", "border", "table", "disabled", "accentText",
9
- "textMuted", "semanticText", "onPrimary", "onSemantic",
8
+ "boxShadow", "focusRing", "sidebar", "boxborder", "border", "table", "disabled",
9
+ "accentText", "textMuted", "semanticText", "onPrimary", "onSemantic",
10
10
  ];
11
11
  exports.allPaletteTokensListed = true;
12
12
  const NESTED = [
@@ -17,6 +17,7 @@ const NESTED = [
17
17
  ]],
18
18
  ["semanticText", ["success", "error", "warning", "info"]],
19
19
  ["onSemantic", ["success", "error", "warning", "info"]],
20
+ ["focusRing", ["ring", "halo"]],
20
21
  ["backgroundColor", ["main", "base"]],
21
22
  ["boxShadow", ["primary", "default"]],
22
23
  ];
@@ -10,6 +10,7 @@ const fonts_web_1 = __importDefault(require("./fonts.web"));
10
10
  const componentStyles_1 = require("./componentStyles");
11
11
  const accentText_1 = require("../tokens/builders/accentText");
12
12
  const scrollbar_1 = require("../tokens/builders/scrollbar");
13
+ const focusRing_styles_1 = require("./focusRing.styles");
13
14
  /**
14
15
  * Builds a Chakra theme from a pure brand palette.
15
16
  *
@@ -27,6 +28,12 @@ const createBrandTheme = (palette, options = {}) => {
27
28
  colors: palette,
28
29
  ...fonts_web_1.default,
29
30
  ...common_web_1.default,
31
+ // `shadows.outline` is what Chakra's stock `_focusVisible` reaches for on EVERY component it
32
+ // themes. The primitive ships one hardcoded blue (`rgba(66,153,225,.6)`), which measured
33
+ // 1.83:1 on the lavender canvas and 1.38:1 on a primary button — below 1.4.11's 3:1 on all
34
+ // nine brands, in both modes. A primitive cannot know the palette; this is the first place
35
+ // that does, so the palette-derived ring is bound here and every stock focus state inherits it.
36
+ shadows: { ...common_web_1.default.shadows, outline: (0, focusRing_styles_1.focusRingShadow)(palette) },
30
37
  };
31
38
  return (0, react_1.extendTheme)({
32
39
  ...brand,
@@ -0,0 +1,52 @@
1
+ import type { FocusRingTokens } from "../tokens/types";
2
+ /**
3
+ * WEB binding for the `focusRing` token pair. The colours are derived in
4
+ * `tokens/builders/focusRing.ts` (pure, shared with the mobile library); the CSS shape —
5
+ * box-shadow bands, the forced-colors outline — is web-only and lives here.
6
+ */
7
+ /** Inner band, in the surface tone: separates the ring from the control's own fill. */
8
+ export declare const FOCUS_HALO_WIDTH = 2;
9
+ /** Outer edge of the brand band. The band itself is the 2px between halo and ring. */
10
+ export declare const FOCUS_RING_WIDTH = 4;
11
+ type WithFocusRing = {
12
+ focusRing: FocusRingTokens;
13
+ };
14
+ /**
15
+ * The two-tone ring as a single `box-shadow`. Drawn OUTSIDE the border box, so it costs no
16
+ * layout and never resizes the control — the reason this is a shadow and not an outline.
17
+ */
18
+ export declare const focusRingShadow: (colors: WithFocusRing) => string;
19
+ /**
20
+ * `:hover`, `:active` and `:focus-visible` all score the same specificity, so which one paints
21
+ * comes down to source order — and in the merged style object Chakra's own Button baseStyle
22
+ * registers `_focusVisible` BEFORE `_hover`. A focused button that is also hovered would then
23
+ * drop the ring for `_hover`'s `boxShadow: "sm"`. Stacking the pseudo-classes raises the
24
+ * specificity above both, so the ring survives whatever else the state repaints.
25
+ */
26
+ export declare const FOCUS_VISIBLE_OVER_STATE = "&:hover:focus-visible, &:active:focus-visible, &[data-active]:focus-visible";
27
+ /**
28
+ * `:focus-visible`, never `:focus` — a mouse click must not draw a ring.
29
+ *
30
+ * The transparent outline is not decoration: in Windows High Contrast / `forced-colors` mode
31
+ * box-shadows are dropped entirely, and a transparent outline is the one thing the OS repaints
32
+ * in its own focus colour. Without it, forced-colors users get no indicator at all.
33
+ */
34
+ export declare const focusVisibleRing: (colors: WithFocusRing) => {
35
+ outline: string;
36
+ outlineOffset: string;
37
+ boxShadow: string;
38
+ };
39
+ /** Both selectors a control needs to keep the ring on top of its own hover/active styling. */
40
+ export declare const focusVisibleStyles: (colors: WithFocusRing) => {
41
+ _focusVisible: {
42
+ outline: string;
43
+ outlineOffset: string;
44
+ boxShadow: string;
45
+ };
46
+ "&:hover:focus-visible, &:active:focus-visible, &[data-active]:focus-visible": {
47
+ outline: string;
48
+ outlineOffset: string;
49
+ boxShadow: string;
50
+ };
51
+ };
52
+ export {};
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.focusVisibleStyles = exports.focusVisibleRing = exports.FOCUS_VISIBLE_OVER_STATE = exports.focusRingShadow = exports.FOCUS_RING_WIDTH = exports.FOCUS_HALO_WIDTH = void 0;
4
+ /**
5
+ * WEB binding for the `focusRing` token pair. The colours are derived in
6
+ * `tokens/builders/focusRing.ts` (pure, shared with the mobile library); the CSS shape —
7
+ * box-shadow bands, the forced-colors outline — is web-only and lives here.
8
+ */
9
+ /** Inner band, in the surface tone: separates the ring from the control's own fill. */
10
+ exports.FOCUS_HALO_WIDTH = 2;
11
+ /** Outer edge of the brand band. The band itself is the 2px between halo and ring. */
12
+ exports.FOCUS_RING_WIDTH = 4;
13
+ /**
14
+ * The two-tone ring as a single `box-shadow`. Drawn OUTSIDE the border box, so it costs no
15
+ * layout and never resizes the control — the reason this is a shadow and not an outline.
16
+ */
17
+ const focusRingShadow = (colors) => `0 0 0 ${exports.FOCUS_HALO_WIDTH}px ${colors.focusRing.halo}, ` +
18
+ `0 0 0 ${exports.FOCUS_RING_WIDTH}px ${colors.focusRing.ring}`;
19
+ exports.focusRingShadow = focusRingShadow;
20
+ /**
21
+ * `:hover`, `:active` and `:focus-visible` all score the same specificity, so which one paints
22
+ * comes down to source order — and in the merged style object Chakra's own Button baseStyle
23
+ * registers `_focusVisible` BEFORE `_hover`. A focused button that is also hovered would then
24
+ * drop the ring for `_hover`'s `boxShadow: "sm"`. Stacking the pseudo-classes raises the
25
+ * specificity above both, so the ring survives whatever else the state repaints.
26
+ */
27
+ exports.FOCUS_VISIBLE_OVER_STATE = "&:hover:focus-visible, &:active:focus-visible, &[data-active]:focus-visible";
28
+ /**
29
+ * `:focus-visible`, never `:focus` — a mouse click must not draw a ring.
30
+ *
31
+ * The transparent outline is not decoration: in Windows High Contrast / `forced-colors` mode
32
+ * box-shadows are dropped entirely, and a transparent outline is the one thing the OS repaints
33
+ * in its own focus colour. Without it, forced-colors users get no indicator at all.
34
+ */
35
+ const focusVisibleRing = (colors) => ({
36
+ outline: "2px solid transparent",
37
+ outlineOffset: "2px",
38
+ boxShadow: (0, exports.focusRingShadow)(colors),
39
+ });
40
+ exports.focusVisibleRing = focusVisibleRing;
41
+ /** Both selectors a control needs to keep the ring on top of its own hover/active styling. */
42
+ const focusVisibleStyles = (colors) => {
43
+ const ring = (0, exports.focusVisibleRing)(colors);
44
+ return { _focusVisible: ring, [exports.FOCUS_VISIBLE_OVER_STATE]: ring };
45
+ };
46
+ exports.focusVisibleStyles = focusVisibleStyles;
@@ -46,6 +46,7 @@ const sidebarAccent_1 = require("../builders/sidebarAccent");
46
46
  const mutedText_1 = require("../builders/mutedText");
47
47
  const semanticText_1 = require("../builders/semanticText");
48
48
  const resolvedRungs_1 = require("../builders/resolvedRungs");
49
+ const focusRing_1 = require("../builders/focusRing");
49
50
  const surfaceRungs_1 = require("../builders/surfaceRungs");
50
51
  /** Derived, never hand-written: a copied hex goes stale the moment a canvas moves. */
51
52
  const finalizeLightPalette = (source) => ({
@@ -56,9 +57,13 @@ const finalizeLightPalette = (source) => ({
56
57
  semanticText: (0, semanticText_1.buildSemanticText)(source.semantic, source.backgroundColor.main),
57
58
  onPrimary: (0, accentText_1.onFilled)(source.primary[500], source.white, source.black),
58
59
  onSemantic: (0, semanticText_1.buildOnSemantic)(source.semantic, source.white, source.black),
60
+ focusRing: (0, focusRing_1.buildFocusRing)(source),
59
61
  });
60
62
  exports.finalizeLightPalette = finalizeLightPalette;
61
- const resolveRungs = (palette) => (0, surfaceRungs_1.withSurfaceRungs)((0, resolvedRungs_1.withTableHoverRung)((0, resolvedRungs_1.withPlaceholderRung)((0, outlineRung_1.withOutlineRung)(palette))));
63
+ // `withFocusRing` is outermost: it is the only step that reads `primary` and the surfaces
64
+ // TOGETHER, so it must see both after every earlier step — and after a brand file's own
65
+ // post-derivation overrides (zinc dark replaces `primary` and `background` wholesale).
66
+ const resolveRungs = (palette) => (0, focusRing_1.withFocusRing)((0, surfaceRungs_1.withSurfaceRungs)((0, resolvedRungs_1.withTableHoverRung)((0, resolvedRungs_1.withPlaceholderRung)((0, outlineRung_1.withOutlineRung)(palette)))));
62
67
  const lightBrand = (source) => resolveRungs((0, exports.finalizeLightPalette)(source));
63
68
  /** Light palette per brand. `lavender` is the base the other six extend. */
64
69
  exports.lightPalettes = {
@@ -197,6 +197,7 @@ declare const palette: {
197
197
  purple: import("../..").ColorScale;
198
198
  pink: import("../..").ColorScale;
199
199
  backgroundColor: import("../..").NamedSurfaces;
200
+ focusRing: import("../..").FocusRingTokens;
200
201
  sidebar: import("../..").SidebarTokens;
201
202
  semanticText: import("../..").SemanticTextTokens;
202
203
  textMuted: string;
@@ -8,6 +8,7 @@ const accentText_1 = require("./accentText");
8
8
  const sidebarAccent_1 = require("./sidebarAccent");
9
9
  const mutedText_1 = require("./mutedText");
10
10
  const semanticText_1 = require("./semanticText");
11
+ const focusRing_1 = require("./focusRing");
11
12
  /**
12
13
  * Derives a brand's dark palette from its light palette.
13
14
  *
@@ -246,6 +247,7 @@ function buildDarkPalette(light) {
246
247
  ]));
247
248
  const white = light.white;
248
249
  const primary = liftBrand(light.primary, white);
250
+ const background = fromTargets(hueSource, SURFACE_TARGETS, SURFACE_SAT);
249
251
  const semantic = {
250
252
  success: liftAccent(light.semantic.success, white),
251
253
  error: liftAccent(light.semantic.error, white),
@@ -276,7 +278,7 @@ function buildDarkPalette(light) {
276
278
  purple: (0, exports.reverseScale)(light.purple),
277
279
  pink: (0, exports.reverseScale)(light.pink),
278
280
  backgroundColor: namedSurfaces,
279
- background: fromTargets(hueSource, SURFACE_TARGETS, SURFACE_SAT),
281
+ background,
280
282
  text: fromTargets(hueSource, TEXT_TARGETS, TEXT_SAT),
281
283
  header: fromTargets(hueSource, TEXT_TARGETS, TEXT_SAT + 2),
282
284
  placeholder: fromTargets(hueSource, PLACEHOLDER_TARGETS, PLACEHOLDER_SAT),
@@ -293,6 +295,9 @@ function buildDarkPalette(light) {
293
295
  semanticText: (0, semanticText_1.buildSemanticText)(semantic, namedSurfaces.main),
294
296
  onPrimary: (0, accentText_1.onFilled)(primary[500], light.white, light.black),
295
297
  onSemantic: (0, semanticText_1.buildOnSemantic)(semantic, light.white, light.black),
298
+ // The dark ring is derived from the LIFTED accent against the dark canvas, not copied from
299
+ // light: the two modes squeeze it from opposite sides, so a shared value fails one of them.
300
+ focusRing: (0, focusRing_1.buildFocusRing)({ primary, background, backgroundColor: namedSurfaces }),
296
301
  // Sidebar is already near-black in light mode, so it needs no inversion.
297
302
  sidebar: (0, sidebarAccent_1.withSidebarTokens)(light),
298
303
  boxShadow: {