@12-apps/ui 6.5.0 → 6.7.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.
Files changed (86) hide show
  1. package/dist/{chunk-3KWD5Y5O.js → chunk-RXE5CUUE.js} +8 -4
  2. package/dist/chunk-RXE5CUUE.js.map +1 -0
  3. package/dist/data-display/Alert.js +1 -1
  4. package/dist/data-display/AsyncStateContainer.js +2 -1
  5. package/dist/data-display/AsyncStateContainer.js.map +1 -1
  6. package/dist/data-display/Banner.js +4 -2
  7. package/dist/data-display/Banner.js.map +1 -1
  8. package/dist/data-display/CardKit.js +3 -1
  9. package/dist/data-display/CardKit.js.map +1 -1
  10. package/dist/data-display/Carousel.js +5 -2
  11. package/dist/data-display/Carousel.js.map +1 -1
  12. package/dist/data-display/DataViews.js +8 -7
  13. package/dist/data-display/DataViews.js.map +1 -1
  14. package/dist/data-display/Lightbox.js +147 -117
  15. package/dist/data-display/Lightbox.js.map +1 -1
  16. package/dist/data-display/MapPreview.js +14 -10
  17. package/dist/data-display/MapPreview.js.map +1 -1
  18. package/dist/pt-BR.js +46 -3
  19. package/dist/pt-BR.js.map +1 -1
  20. package/dist/types/components/data-display/Alert/Alert.d.ts +9 -2
  21. package/dist/types/components/data-display/Alert/Alert.d.ts.map +1 -1
  22. package/dist/types/components/data-display/Alert/Alert.types.d.ts +25 -9
  23. package/dist/types/components/data-display/Alert/Alert.types.d.ts.map +1 -1
  24. package/dist/types/components/data-display/AsyncStateContainer/AsyncStateContainer.d.ts.map +1 -1
  25. package/dist/types/components/data-display/AsyncStateContainer/AsyncStateContainer.types.d.ts +6 -0
  26. package/dist/types/components/data-display/AsyncStateContainer/AsyncStateContainer.types.d.ts.map +1 -1
  27. package/dist/types/components/data-display/Banner/Banner.d.ts.map +1 -1
  28. package/dist/types/components/data-display/Banner/Banner.types.d.ts +2 -0
  29. package/dist/types/components/data-display/Banner/Banner.types.d.ts.map +1 -1
  30. package/dist/types/components/data-display/CardKit/card-actions-context.d.ts +3 -1
  31. package/dist/types/components/data-display/CardKit/card-actions-context.d.ts.map +1 -1
  32. package/dist/types/components/data-display/Carousel/Carousel.chrome.d.ts +3 -0
  33. package/dist/types/components/data-display/Carousel/Carousel.chrome.d.ts.map +1 -1
  34. package/dist/types/components/data-display/Carousel/Carousel.parts.d.ts.map +1 -1
  35. package/dist/types/components/data-display/Carousel/Carousel.types.d.ts +7 -0
  36. package/dist/types/components/data-display/Carousel/Carousel.types.d.ts.map +1 -1
  37. package/dist/types/components/data-display/DataViews/data-views-copy.d.ts +25 -0
  38. package/dist/types/components/data-display/DataViews/data-views-copy.d.ts.map +1 -1
  39. package/dist/types/components/data-display/DataViews/data-views-layout-context.d.ts.map +1 -1
  40. package/dist/types/components/data-display/DataViews/data-views-table-parts.d.ts.map +1 -1
  41. package/dist/types/components/data-display/Lightbox/Lightbox.d.ts.map +1 -1
  42. package/dist/types/components/data-display/Lightbox/Lightbox.types.d.ts +9 -0
  43. package/dist/types/components/data-display/Lightbox/Lightbox.types.d.ts.map +1 -1
  44. package/dist/types/components/data-display/Lightbox/LightboxControls.d.ts.map +1 -1
  45. package/dist/types/components/data-display/Lightbox/lightbox-copy-context.d.ts +17 -0
  46. package/dist/types/components/data-display/Lightbox/lightbox-copy-context.d.ts.map +1 -0
  47. package/dist/types/components/data-display/MapPreview/MapControlBar.d.ts +7 -0
  48. package/dist/types/components/data-display/MapPreview/MapControlBar.d.ts.map +1 -1
  49. package/dist/types/components/data-display/MapPreview/MapLayers.d.ts.map +1 -1
  50. package/dist/types/components/data-display/MapPreview/MapPreview.types.d.ts +9 -0
  51. package/dist/types/components/data-display/MapPreview/MapPreview.types.d.ts.map +1 -1
  52. package/dist/types/components/data-display/MapPreview/MapSearchBar.d.ts +2 -0
  53. package/dist/types/components/data-display/MapPreview/MapSearchBar.d.ts.map +1 -1
  54. package/dist/types/copy.d.ts +53 -0
  55. package/dist/types/copy.d.ts.map +1 -1
  56. package/dist/types/pt-BR.d.ts +5 -1
  57. package/dist/types/pt-BR.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/components/data-display/Alert/Alert.tsx +22 -5
  60. package/src/components/data-display/Alert/Alert.types.ts +20 -10
  61. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.tsx +2 -1
  62. package/src/components/data-display/AsyncStateContainer/AsyncStateContainer.types.ts +6 -0
  63. package/src/components/data-display/Banner/Banner.tsx +5 -2
  64. package/src/components/data-display/Banner/Banner.types.ts +2 -0
  65. package/src/components/data-display/CardKit/card-actions-context.tsx +4 -0
  66. package/src/components/data-display/Carousel/Carousel.chrome.tsx +5 -0
  67. package/src/components/data-display/Carousel/Carousel.parts.tsx +4 -3
  68. package/src/components/data-display/Carousel/Carousel.types.ts +7 -0
  69. package/src/components/data-display/DataViews/data-views-copy.ts +25 -0
  70. package/src/components/data-display/DataViews/data-views-display-tabs.tsx +2 -2
  71. package/src/components/data-display/DataViews/data-views-export.tsx +1 -1
  72. package/src/components/data-display/DataViews/data-views-layout-context.tsx +3 -1
  73. package/src/components/data-display/DataViews/data-views-more-filters.tsx +2 -2
  74. package/src/components/data-display/DataViews/data-views-range-pill.tsx +1 -1
  75. package/src/components/data-display/DataViews/data-views-table-parts.tsx +1 -0
  76. package/src/components/data-display/Lightbox/Lightbox.tsx +3 -0
  77. package/src/components/data-display/Lightbox/Lightbox.types.ts +9 -0
  78. package/src/components/data-display/Lightbox/LightboxControls.tsx +28 -15
  79. package/src/components/data-display/Lightbox/lightbox-copy-context.tsx +42 -0
  80. package/src/components/data-display/MapPreview/MapControlBar.tsx +17 -9
  81. package/src/components/data-display/MapPreview/MapLayers.tsx +2 -0
  82. package/src/components/data-display/MapPreview/MapPreview.types.ts +10 -0
  83. package/src/components/data-display/MapPreview/MapSearchBar.tsx +4 -1
  84. package/src/copy.ts +57 -0
  85. package/src/pt-BR.ts +48 -1
  86. package/dist/chunk-3KWD5Y5O.js.map +0 -1
@@ -1,6 +1,12 @@
1
1
  import type React from 'react';
2
2
 
3
3
  export interface AsyncStateContainerProps {
4
+ /**
5
+ * What an EMPTY result reads as, when the caller supplies no
6
+ * `emptyComponent` of its own. REQUIRED — the fallback was the English
7
+ * "No data available", on the package every host renders.
8
+ */
9
+ emptyTitle: string;
4
10
  /**
5
11
  * Whether the container is in a loading state
6
12
  * @default false
@@ -202,8 +202,9 @@ const BannerTrailing: React.FC<{
202
202
  variant: BannerVariant;
203
203
  dismissible: boolean;
204
204
  onDismissClick: () => void;
205
+ dismissLabel: string;
205
206
  dataTestId?: string;
206
- }> = ({ actions, variant, dismissible, onDismissClick, dataTestId }) => (
207
+ }> = ({ actions, variant, dismissible, onDismissClick, dismissLabel, dataTestId }) => (
207
208
  <Box display="flex" alignItems="center" gap={1}>
208
209
  {actions && actions.length > 0 && (
209
210
  <Box className="banner-actions">
@@ -223,7 +224,7 @@ const BannerTrailing: React.FC<{
223
224
  className="banner-dismiss"
224
225
  size="small"
225
226
  onClick={onDismissClick}
226
- aria-label="Dismiss banner"
227
+ aria-label={dismissLabel}
227
228
  data-testid={testIdFor(dataTestId, 'close')}
228
229
  sx={{ transition: 'all 0.2s ease' }}
229
230
  >
@@ -241,6 +242,7 @@ export const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
241
242
  description,
242
243
  icon,
243
244
  dismissible = false,
245
+ dismissLabel,
244
246
  onDismiss,
245
247
  actions,
246
248
  sticky = false,
@@ -295,6 +297,7 @@ export const Banner = React.forwardRef<HTMLDivElement, BannerProps>(
295
297
  variant={variant}
296
298
  dismissible={dismissible}
297
299
  onDismissClick={handleDismiss}
300
+ dismissLabel={dismissLabel}
298
301
  dataTestId={dataTestId}
299
302
  />
300
303
  </StyledBanner>
@@ -14,6 +14,8 @@ export interface BannerAction {
14
14
  }
15
15
 
16
16
  export interface BannerProps {
17
+ /** The dismiss button's accessible name — it carries a glyph only. REQUIRED. */
18
+ dismissLabel: string;
17
19
  /** Message severity type */
18
20
  variant?: BannerVariant;
19
21
  /** Main message title text */
@@ -37,6 +37,7 @@ export function CardActionsProvider({
37
37
  tenantSlug,
38
38
  onRefresh,
39
39
  errorTitle,
40
+ errorDismissLabel,
40
41
  autoHideMs = 6_000,
41
42
  children,
42
43
  }: {
@@ -50,6 +51,8 @@ export function CardActionsProvider({
50
51
  * MESSAGE beside it comes from whatever failed, which is already the host's.
51
52
  */
52
53
  errorTitle: string;
54
+ /** The dismiss on that alert, which carries a glyph only. REQUIRED. */
55
+ errorDismissLabel: string;
53
56
  /** How long the snackbar stays up. */
54
57
  autoHideMs?: number;
55
58
  children: ReactNode;
@@ -72,6 +75,7 @@ export function CardActionsProvider({
72
75
  title={errorTitle}
73
76
  description={error ?? ''}
74
77
  closable
78
+ closeLabel={errorDismissLabel}
75
79
  onClose={() => setError(null)}
76
80
  data-testid="card-action-error"
77
81
  />
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
 
3
3
  import { CarouselArrows, CarouselIndicators, CarouselThumbnails } from './Carousel.parts';
4
+ import type { CarouselCopy } from '../../../copy';
4
5
  import type {
5
6
  CarouselArrowsProps,
6
7
  CarouselIndicatorsProps,
@@ -12,6 +13,8 @@ type IndicatorPosition = NonNullable<CarouselIndicatorsProps['position']>;
12
13
  type Color = NonNullable<CarouselIndicatorsProps['color']>;
13
14
 
14
15
  export interface CarouselChromeProps {
16
+ /** The two arrows' accessible names, from the mount. */
17
+ copy: CarouselCopy;
15
18
  items: CarouselItem[];
16
19
  activeIndex: number;
17
20
  loop: boolean;
@@ -34,6 +37,7 @@ export interface CarouselChromeProps {
34
37
  * as three conditional blocks inside the carousel's own render.
35
38
  */
36
39
  export const CarouselChrome: React.FC<CarouselChromeProps> = ({
40
+ copy,
37
41
  items,
38
42
  activeIndex,
39
43
  loop,
@@ -52,6 +56,7 @@ export const CarouselChrome: React.FC<CarouselChromeProps> = ({
52
56
  <>
53
57
  {showArrows && !disabled && (
54
58
  <CarouselArrows
59
+ copy={copy}
55
60
  onPrev={onPrev}
56
61
  onNext={onNext}
57
62
  position={arrowPosition}
@@ -212,7 +212,8 @@ const arrowSx = (theme: Theme, color: Color, isOverlay: boolean): CSSObject => (
212
212
 
213
213
  export const CarouselArrows: React.FC<CarouselArrowsProps> = (props) => {
214
214
  const {
215
- onPrev, onNext, position, disabled, disablePrev, disableNext, color, size, className, style,
215
+ copy, onPrev, onNext, position, disabled, disablePrev, disableNext, color, size, className,
216
+ style,
216
217
  } = withDefaults(props, ARROWS_DEFAULTS) as CarouselArrowsProps &
217
218
  Required<Pick<CarouselArrowsProps, keyof typeof ARROWS_DEFAULTS>>;
218
219
 
@@ -227,7 +228,7 @@ export const CarouselArrows: React.FC<CarouselArrowsProps> = (props) => {
227
228
  disabled={disabled || disablePrev}
228
229
  size={muiSize(size)}
229
230
  className={className}
230
- aria-label="Previous slide"
231
+ aria-label={copy.previous}
231
232
  data-testid="carousel-prev-button"
232
233
  sx={{ ...sx, left: offset, ...style }}
233
234
  >
@@ -237,7 +238,7 @@ export const CarouselArrows: React.FC<CarouselArrowsProps> = (props) => {
237
238
  <IconButton
238
239
  onClick={onNext}
239
240
  disabled={disabled || disableNext}
240
- aria-label="Next slide"
241
+ aria-label={copy.next}
241
242
  size={muiSize(size)}
242
243
  className={className}
243
244
  data-testid="carousel-next-button"
@@ -1,5 +1,6 @@
1
1
  import type { ColorValue, SizeValue } from '../../../tokens/scales';
2
2
  import type { CSSProperties, ReactNode } from 'react';
3
+ import type { CarouselCopy } from '../../../copy';
3
4
 
4
5
  export interface CarouselItem {
5
6
  id: string;
@@ -11,6 +12,11 @@ export interface CarouselItem {
11
12
  }
12
13
 
13
14
  export interface CarouselProps {
15
+ /**
16
+ * The two arrows' accessible names. They render a glyph and nothing else,
17
+ * so this pair IS what a screen reader announces for them. REQUIRED.
18
+ */
19
+ copy: CarouselCopy;
14
20
  items: CarouselItem[];
15
21
  variant?: 'default' | 'glass' | 'gradient' | 'elevated' | 'minimal' | 'cards';
16
22
  size?: SizeValue;
@@ -58,6 +64,7 @@ export interface CarouselIndicatorsProps {
58
64
  }
59
65
 
60
66
  export interface CarouselArrowsProps {
67
+ copy: CarouselCopy;
61
68
  onPrev: () => void;
62
69
  onNext: () => void;
63
70
  position?: 'inside' | 'outside' | 'overlay';
@@ -122,6 +122,11 @@ export interface DataViewsDisplayCopy {
122
122
  densityUnavailableNarrow: string;
123
123
  /** The control that opens this panel. */
124
124
  trigger: string;
125
+ /** The two column headings inside the panel: which field, and in what shape. */
126
+ fieldHeading: string;
127
+ formatHeading: string;
128
+ /** The card-grid zoom slider, which carries a glyph at each end and no label. */
129
+ cardZoom: string;
125
130
  }
126
131
 
127
132
  export interface DataViewsExportCopy {
@@ -130,6 +135,12 @@ export interface DataViewsExportCopy {
130
135
  visibleColumns(columnCount: number): string;
131
136
  /** The control that opens the menu. */
132
137
  trigger: string;
138
+ /**
139
+ * The word ON that control when the bar has room for it. `trigger` is its
140
+ * accessible name and its compact tooltip; this is what the eye reads, and
141
+ * it was a literal — so a host could set one and not the other.
142
+ */
143
+ triggerLabel: string;
133
144
  }
134
145
 
135
146
  export interface DataViewsGridCopy {
@@ -151,9 +162,21 @@ export interface DataViewsGridCopy {
151
162
  export interface DataViewsFiltersCopy {
152
163
  moreFilters(count: number): string;
153
164
  moreFiltersApplied(count: number, appliedCount: number): string;
165
+ /**
166
+ * The overflow control's own two words: the one ON the button, and the
167
+ * heading of the sheet it opens. `moreFilters` is the accessible name and
168
+ * counts; these are what the eye reads.
169
+ */
170
+ moreTriggerLabel: string;
171
+ moreHeading: string;
154
172
  overflowNote: string;
155
173
  rangeEnd: string;
156
174
  rangeInvalid: string;
175
+ /**
176
+ * A day range includes BOTH endpoints, which a reader cannot tell from two
177
+ * dates alone — and getting it wrong silently drops or adds a day's rows.
178
+ */
179
+ rangeInclusiveNote: string;
157
180
  /** The day-range presets' labels, keyed by preset id. */
158
181
  rangePresets: Readonly<Record<string, string>>;
159
182
  scopesLabel: string;
@@ -208,6 +231,8 @@ export interface DataViewsNavCopy {
208
231
  reset: string;
209
232
  /** The inline error when a view could not be refreshed or saved. */
210
233
  updateFailed: string;
234
+ /** The dismiss on that failure's alert — a glyph with no visible label. */
235
+ dismissError: string;
211
236
  saveFailed: string;
212
237
  /** Reopen the save dialog over an existing view. */
213
238
  editView: string;
@@ -151,7 +151,7 @@ export function SortTab({
151
151
  </Box>
152
152
  <Text variant="caption" as="p">
153
153
  <Box component="span" sx={{ px: 0.5, color: "text.secondary" }}>
154
- Campo
154
+ {copy.display.fieldHeading}
155
155
  </Box>
156
156
  </Text>
157
157
  {fields.map((field) => (
@@ -326,7 +326,7 @@ export function DisplayTab({ testIdPrefix }: { testIdPrefix: string }): React.JS
326
326
  <Box sx={{ p: 1 }}>
327
327
  <Text variant="caption" as="p">
328
328
  <Box component="span" sx={{ px: 0.5, color: "text.secondary" }}>
329
- Formato
329
+ {copy.display.formatHeading}
330
330
  </Box>
331
331
  </Text>
332
332
  <Box
@@ -110,7 +110,7 @@ function ExportTrigger({
110
110
  {busy ? <CircularProgress size={14} /> : <DownloadRoundedIcon fontSize="small" />}
111
111
  {!compact && (
112
112
  <Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
113
- Exportar
113
+ {copy.export.triggerLabel}
114
114
  </Box>
115
115
  )}
116
116
  {/* Dropped with the label — see the note on the Exibir trigger. */}
@@ -7,6 +7,7 @@ import GridViewOutlinedIcon from "@mui/icons-material/GridViewOutlined";
7
7
  import { Slider } from "../../form/Slider";
8
8
  import { Box } from "../../../mui/Box";
9
9
 
10
+ import { useDataViewsCopy } from "./data-views-copy-context";
10
11
  import { DATA_VIEWS_LAYOUTS, type DataViewsLayout } from "./data-views-types";
11
12
 
12
13
  /**
@@ -322,6 +323,7 @@ export function useDataViewsLayout(): DataViewsLayoutValue {
322
323
  */
323
324
  export function DataViewsZoomSlider({ testIdPrefix }: { testIdPrefix: string }): React.JSX.Element | null {
324
325
  const { layout, zoom, setZoom } = useDataViewsLayout();
326
+ const copy = useDataViewsCopy();
325
327
  if (layout !== "cards" && layout !== "board") return null;
326
328
  return (
327
329
  <Box
@@ -342,7 +344,7 @@ export function DataViewsZoomSlider({ testIdPrefix }: { testIdPrefix: string }):
342
344
  max={100}
343
345
  step={5}
344
346
  onChange={(_event, next) => setZoom(Array.isArray(next) ? (next[0] ?? 0) : next)}
345
- aria-label="Tamanho dos cards"
347
+ aria-label={copy.display.cardZoom}
346
348
  data-testid={`${testIdPrefix}-card-zoom`}
347
349
  />
348
350
  </Box>
@@ -86,7 +86,7 @@ function MoreTrigger({
86
86
  <FilterAltRoundedIcon fontSize="small" />
87
87
  {!compact && (
88
88
  <Box component="span" sx={{ display: { xs: "none", sm: "inline" } }}>
89
- Mais
89
+ {copy.filters.moreTriggerLabel}
90
90
  </Box>
91
91
  )}
92
92
  <Box
@@ -126,7 +126,7 @@ function MoreHeading(): React.JSX.Element {
126
126
  >
127
127
  <Text variant="caption" as="span">
128
128
  <Box component="span" sx={{ textTransform: "uppercase", letterSpacing: 0.5, color: "text.disabled" }}>
129
- Mais filtros
129
+ {copy.filters.moreHeading}
130
130
  </Box>
131
131
  </Text>
132
132
  <Text variant="caption" as="span">
@@ -133,7 +133,7 @@ export function RangeBounds<T extends Record<string, unknown>>({
133
133
  data-testid={`${testId}-inclusive`}
134
134
  sx={{ mt: 0.75, fontSize: '0.75rem', color: 'text.disabled' }}
135
135
  >
136
- Ambas as datas entram no resultado.
136
+ {copy.filters.rangeInclusiveNote}
137
137
  </Typography>
138
138
  )}
139
139
  </Box>
@@ -36,6 +36,7 @@ export function ViewMutationErrorAlert({
36
36
  title={copy.nav.updateFailed}
37
37
  description={error}
38
38
  closable
39
+ closeLabel={copy.nav.dismissError}
39
40
  onClose={onClose}
40
41
  data-testid={`${testIdPrefix}-views-mutation-error`}
41
42
  />
@@ -3,6 +3,7 @@ import React from 'react';
3
3
 
4
4
  import { makeTestId } from './Lightbox.constants';
5
5
  import { resolveLightboxProps } from './Lightbox.helpers';
6
+ import { LightboxCopyProvider } from './lightbox-copy-context';
6
7
  import type { LightboxProps, LightboxRef } from './Lightbox.types';
7
8
  import { LightboxCurrentMedia } from './LightboxCurrentMedia';
8
9
  import { LightboxOverlay } from './LightboxOverlay';
@@ -32,6 +33,7 @@ export const Lightbox = React.forwardRef<LightboxRef, LightboxProps>((componentP
32
33
  const { currentIndex, currentItem, zoom } = lightbox;
33
34
 
34
35
  return (
36
+ <LightboxCopyProvider copy={props.copy}>
35
37
  <Dialog
36
38
  open={isOpen}
37
39
  onClose={lightbox.close}
@@ -98,6 +100,7 @@ export const Lightbox = React.forwardRef<LightboxRef, LightboxProps>((componentP
98
100
  }
99
101
  />
100
102
  </Dialog>
103
+ </LightboxCopyProvider>
101
104
  );
102
105
  });
103
106
 
@@ -1,4 +1,5 @@
1
1
  import type { CSSProperties } from 'react';
2
+ import type { LightboxCopy } from '../../../copy';
2
3
 
3
4
  export interface LightboxItem {
4
5
  src: string;
@@ -12,6 +13,14 @@ export interface AutoplayConfig {
12
13
  }
13
14
 
14
15
  export interface LightboxProps {
16
+ /**
17
+ * Every word this viewer renders — all of them aria-labels on glyph-only
18
+ * controls, so this object IS what a screen-reader user hears for the whole
19
+ * component. REQUIRED: a default could only be one language read aloud to
20
+ * everybody else's users, and the sighted path looks identical either way,
21
+ * which is why the English literals survived so long.
22
+ */
23
+ copy: LightboxCopy;
15
24
  /** Controls visibility; must lock body scroll when true */
16
25
  isOpen: boolean;
17
26
  /** Fired on close button, backdrop click, Esc, and swipe-down (mobile) */
@@ -11,22 +11,26 @@ import type { FC } from 'react';
11
11
  import React from 'react';
12
12
 
13
13
  import { navButtonSx, OVERLAY_Z_INDEX, overlayButtonSx } from './Lightbox.constants';
14
+ import { useLightboxCopy } from './lightbox-copy-context';
14
15
 
15
16
  type TestId = (suffix: string) => string;
16
17
 
17
18
  export const LightboxCloseButton: FC<{ testId: TestId; onClose: () => void }> = ({
18
19
  testId,
19
20
  onClose,
20
- }) => (
21
+ }) => {
22
+ const copy = useLightboxCopy();
23
+ return (
21
24
  <IconButton
22
25
  onClick={onClose}
23
26
  sx={{ ...overlayButtonSx, position: 'absolute', top: 16, right: 16, zIndex: OVERLAY_Z_INDEX }}
24
- aria-label="Close lightbox"
27
+ aria-label={copy.close}
25
28
  data-testid={testId('close')}
26
29
  >
27
30
  <CloseIcon />
28
31
  </IconButton>
29
- );
32
+ );
33
+ };
30
34
 
31
35
  const arrowSx = (side: 'left' | 'right') =>
32
36
  ({
@@ -52,13 +56,15 @@ export const LightboxNavButtons: FC<LightboxNavButtonsProps> = ({
52
56
  atEnd,
53
57
  onPrev,
54
58
  onNext,
55
- }) => (
59
+ }) => {
60
+ const copy = useLightboxCopy();
61
+ return (
56
62
  <>
57
63
  <IconButton
58
64
  onClick={onPrev}
59
65
  disabled={atStart}
60
66
  sx={arrowSx('left')}
61
- aria-label="Previous item"
67
+ aria-label={copy.previous}
62
68
  data-testid={testId('prev')}
63
69
  >
64
70
  <PrevIcon />
@@ -68,13 +74,14 @@ export const LightboxNavButtons: FC<LightboxNavButtonsProps> = ({
68
74
  onClick={onNext}
69
75
  disabled={atEnd}
70
76
  sx={arrowSx('right')}
71
- aria-label="Next item"
77
+ aria-label={copy.next}
72
78
  data-testid={testId('next')}
73
79
  >
74
80
  <NextIcon />
75
81
  </IconButton>
76
82
  </>
77
- );
83
+ );
84
+ };
78
85
 
79
86
  export interface LightboxZoomControlsProps {
80
87
  testId: TestId;
@@ -88,7 +95,9 @@ export const LightboxZoomControls: FC<LightboxZoomControlsProps> = ({
88
95
  onZoomIn,
89
96
  onZoomOut,
90
97
  onResetZoom,
91
- }) => (
98
+ }) => {
99
+ const copy = useLightboxCopy();
100
+ return (
92
101
  <Box
93
102
  sx={{
94
103
  position: 'absolute',
@@ -101,13 +110,13 @@ export const LightboxZoomControls: FC<LightboxZoomControlsProps> = ({
101
110
  }}
102
111
  data-testid={testId('zoom-controls')}
103
112
  >
104
- <IconButton onClick={onZoomIn} sx={overlayButtonSx} aria-label="Zoom in" data-testid={testId('zoom-in')}>
113
+ <IconButton onClick={onZoomIn} sx={overlayButtonSx} aria-label={copy.zoomIn} data-testid={testId('zoom-in')}>
105
114
  <ZoomInIcon />
106
115
  </IconButton>
107
116
  <IconButton
108
117
  onClick={onZoomOut}
109
118
  sx={overlayButtonSx}
110
- aria-label="Zoom out"
119
+ aria-label={copy.zoomOut}
111
120
  data-testid={testId('zoom-out')}
112
121
  >
113
122
  <ZoomOutIcon />
@@ -115,13 +124,14 @@ export const LightboxZoomControls: FC<LightboxZoomControlsProps> = ({
115
124
  <IconButton
116
125
  onClick={onResetZoom}
117
126
  sx={overlayButtonSx}
118
- aria-label="Reset zoom"
127
+ aria-label={copy.resetZoom}
119
128
  data-testid={testId('zoom-reset')}
120
129
  >
121
130
  <ResetZoomIcon />
122
131
  </IconButton>
123
132
  </Box>
124
- );
133
+ );
134
+ };
125
135
 
126
136
  export interface LightboxAutoplayButtonProps {
127
137
  testId: TestId;
@@ -133,7 +143,9 @@ export const LightboxAutoplayButton: FC<LightboxAutoplayButtonProps> = ({
133
143
  testId,
134
144
  isAutoPlaying,
135
145
  onToggle,
136
- }) => (
146
+ }) => {
147
+ const copy = useLightboxCopy();
148
+ return (
137
149
  <IconButton
138
150
  onClick={onToggle}
139
151
  sx={{
@@ -144,9 +156,10 @@ export const LightboxAutoplayButton: FC<LightboxAutoplayButtonProps> = ({
144
156
  transform: 'translateX(-50%)',
145
157
  zIndex: OVERLAY_Z_INDEX,
146
158
  }}
147
- aria-label={isAutoPlaying ? 'Pause slideshow' : 'Start slideshow'}
159
+ aria-label={isAutoPlaying ? copy.pause : copy.play}
148
160
  data-testid={testId('autoplay')}
149
161
  >
150
162
  {isAutoPlaying ? <PauseIcon /> : <PlayIcon />}
151
163
  </IconButton>
152
- );
164
+ );
165
+ };
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react';
4
+ import React from 'react';
5
+
6
+ import type { LightboxCopy } from '../../../copy';
7
+
8
+ /**
9
+ * The lightbox's words, for the four control components that render them.
10
+ *
11
+ * A CONTEXT rather than props: the viewer is `Lightbox` → `LightboxOverlay` →
12
+ * three separate control clusters, and threading one object down three hops is
13
+ * how a copy port comes to exist, be required, and go unread. It stays a
14
+ * single REQUIRED prop at the mount, which is the only place a host answers.
15
+ */
16
+ const LightboxCopyContext = createContext<LightboxCopy | null>(null);
17
+
18
+ export function LightboxCopyProvider({
19
+ copy,
20
+ children,
21
+ }: {
22
+ copy: LightboxCopy;
23
+ children: ReactNode;
24
+ }): React.JSX.Element {
25
+ return <LightboxCopyContext.Provider value={copy}>{children}</LightboxCopyContext.Provider>;
26
+ }
27
+
28
+ /**
29
+ * The words this viewer renders — THROWS outside a provider rather than
30
+ * falling back.
31
+ *
32
+ * Every one of them is an aria-label on a glyph, so a fallback would be the
33
+ * one failure nobody sees in review: the sighted path renders identically
34
+ * whichever language it is in.
35
+ */
36
+ export function useLightboxCopy(): LightboxCopy {
37
+ const copy = useContext(LightboxCopyContext);
38
+ if (!copy) {
39
+ throw new Error('useLightboxCopy must be rendered inside a <LightboxCopyProvider>');
40
+ }
41
+ return copy;
42
+ }
@@ -9,6 +9,7 @@ import React from 'react';
9
9
 
10
10
  import { MAX_ZOOM, MIN_ZOOM } from './MapPreview.constants';
11
11
  import type { MapType } from './MapPreview.types';
12
+ import type { MapPreviewCopy } from '../../../copy';
12
13
 
13
14
  // Kept module-local: the inferred type of a styled() component cannot be named
14
15
  // across a module boundary here (TS2742).
@@ -41,6 +42,12 @@ const ControlButton = styled(IconButton)(({ theme }) => ({
41
42
  }));
42
43
 
43
44
  export interface MapControlBarProps {
45
+ /**
46
+ * The six controls' names. Every one is a glyph with a `title` AND an
47
+ * `aria-label`, so this object is both the tooltip and what a screen reader
48
+ * reads — REQUIRED, because a default is one language for every adopter.
49
+ */
50
+ copy: MapPreviewCopy;
44
51
  zoom: number;
45
52
  mapType: MapType;
46
53
  onZoomIn: () => void;
@@ -51,6 +58,7 @@ export interface MapControlBarProps {
51
58
  }
52
59
 
53
60
  export const MapControlBar: FC<MapControlBarProps> = ({
61
+ copy,
54
62
  zoom,
55
63
  mapType,
56
64
  onZoomIn,
@@ -60,33 +68,33 @@ export const MapControlBar: FC<MapControlBarProps> = ({
60
68
  onToggleFullscreen,
61
69
  }) => (
62
70
  <MapControls data-testid="map-controls">
63
- <Tooltip title="Zoom in" placement="left">
71
+ <Tooltip title={copy.zoomIn} placement="left">
64
72
  <ControlButton
65
73
  size="small"
66
74
  onClick={onZoomIn}
67
- aria-label="Zoom in"
75
+ aria-label={copy.zoomIn}
68
76
  data-testid="zoom-in-button"
69
77
  disabled={zoom >= MAX_ZOOM}
70
78
  >
71
79
  <ZoomInIcon fontSize="small" />
72
80
  </ControlButton>
73
81
  </Tooltip>
74
- <Tooltip title="Zoom out" placement="left">
82
+ <Tooltip title={copy.zoomOut} placement="left">
75
83
  <ControlButton
76
84
  size="small"
77
85
  onClick={onZoomOut}
78
- aria-label="Zoom out"
86
+ aria-label={copy.zoomOut}
79
87
  data-testid="zoom-out-button"
80
88
  disabled={zoom <= MIN_ZOOM}
81
89
  >
82
90
  <ZoomOutIcon fontSize="small" />
83
91
  </ControlButton>
84
92
  </Tooltip>
85
- <Tooltip title="Center map" placement="left">
93
+ <Tooltip title={copy.center} placement="left">
86
94
  <ControlButton
87
95
  size="small"
88
96
  onClick={onCenter}
89
- aria-label="Center map"
97
+ aria-label={copy.center}
90
98
  data-testid="center-button"
91
99
  >
92
100
  <CenterIcon fontSize="small" />
@@ -96,17 +104,17 @@ export const MapControlBar: FC<MapControlBarProps> = ({
96
104
  <ControlButton
97
105
  size="small"
98
106
  onClick={onToggleMapType}
99
- aria-label="Change map type"
107
+ aria-label={copy.mapType}
100
108
  data-testid="map-type-button"
101
109
  >
102
110
  <LayersIcon fontSize="small" />
103
111
  </ControlButton>
104
112
  </Tooltip>
105
- <Tooltip title="Toggle fullscreen" placement="left">
113
+ <Tooltip title={copy.fullscreen} placement="left">
106
114
  <ControlButton
107
115
  size="small"
108
116
  onClick={onToggleFullscreen}
109
- aria-label="Toggle fullscreen"
117
+ aria-label={copy.fullscreen}
110
118
  data-testid="fullscreen-button"
111
119
  >
112
120
  <FullscreenIcon fontSize="small" />
@@ -46,6 +46,7 @@ export const MapLayers: FC<MapLayersProps> = ({ settings, zoom, mapType, search,
46
46
 
47
47
  {settings.showSearch && (
48
48
  <MapSearchBar
49
+ label={settings.copy.search}
49
50
  value={search.searchValue}
50
51
  placeholder={settings.searchPlaceholder}
51
52
  isSearching={search.isSearching}
@@ -56,6 +57,7 @@ export const MapLayers: FC<MapLayersProps> = ({ settings, zoom, mapType, search,
56
57
 
57
58
  {settings.showControls && (
58
59
  <MapControlBar
60
+ copy={settings.copy}
59
61
  zoom={zoom}
60
62
  mapType={mapType}
61
63
  onZoomIn={controls.handleZoomIn}
@@ -1,3 +1,5 @@
1
+ import type { MapPreviewCopy } from '../../../copy';
2
+
1
3
  export type MapType = 'roadmap' | 'satellite' | 'hybrid' | 'terrain';
2
4
 
3
5
  export interface LatLng {
@@ -20,6 +22,14 @@ export interface HeatmapPoint {
20
22
  }
21
23
 
22
24
  export interface MapPreviewProps {
25
+ /**
26
+ * The control bar's six names and the search field's own. Every control is a
27
+ * glyph carrying a `title` and an `aria-label`, so this is both the tooltip
28
+ * and what a screen reader reads. REQUIRED — `searchPlaceholder` beside it
29
+ * shows the shape this replaces: an English default that read as
30
+ * configurable right up until nobody configured it.
31
+ */
32
+ copy: MapPreviewCopy;
23
33
  /** Map center coordinates */
24
34
  center?: { lat: number; lng: number };
25
35
  /** Legacy coordinates prop (use center instead) */