@spear-ai/spectral 1.17.10 → 1.18.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.
@@ -0,0 +1,220 @@
1
+ 'use client';
2
+ //#region src/components/DirectionalColorWheel/directionalColorWheelMath.ts
3
+ /**
4
+ * Pure geometry + colour-mapping helpers for {@link DirectionalColorWheel}.
5
+ *
6
+ * The wheel is a *legend* for the directional spectrogram, not an independent
7
+ * colour scheme. Its bearing→colour convention is intentionally identical to the
8
+ * CPU compositor `bufferToDirectionalImageData` (Horizon `tile-data.ts`). Keeping
9
+ * this in a small, dependency-free module lets it be unit-tested in isolation and
10
+ * gives consumers (HZN-2656) a single importable source for the convention. See
11
+ * `DirectionalColorWheel.consistency.md` for the full alignment proof.
12
+ */
13
+ /** Sector counts the wheel supports, matching the GRAMS dialog. */
14
+ const SUPPORTED_SECTOR_COUNTS = [
15
+ 4,
16
+ 8,
17
+ 12,
18
+ 16,
19
+ 20,
20
+ 24,
21
+ 32
22
+ ];
23
+ /**
24
+ * Default palette. These are the same OKLCH stops, at the same positions, as the
25
+ * compositor's `DIRECTIONAL_OKLCH_STOP_LIST` — so a wheel rendered with no
26
+ * `colorStops`/`colorForBearing` prop already matches the directional feed's
27
+ * default DIFAR palette. Consumers override this with the exact feed palette.
28
+ */
29
+ const DEFAULT_DIRECTIONAL_COLOR_STOPS = [
30
+ {
31
+ color: "oklch(0.65 0.26 29)",
32
+ position: 0
33
+ },
34
+ {
35
+ color: "oklch(0.7 0.17 55)",
36
+ position: .083
37
+ },
38
+ {
39
+ color: "oklch(0.9 0.21 110)",
40
+ position: .167
41
+ },
42
+ {
43
+ color: "oklch(0.87 0.29 142)",
44
+ position: .333
45
+ },
46
+ {
47
+ color: "oklch(0.45 0.26 264)",
48
+ position: .556
49
+ },
50
+ {
51
+ color: "oklch(0.33 0.18 300)",
52
+ position: .722
53
+ },
54
+ {
55
+ color: "oklch(0.5 0.27 328)",
56
+ position: .889
57
+ },
58
+ {
59
+ color: "oklch(0.65 0.26 29)",
60
+ position: 1
61
+ }
62
+ ];
63
+ const TWO_PI = Math.PI * 2;
64
+ /** Wrap any angle into `[0, 360)`. */
65
+ const normalizeBearing = (degrees) => (degrees % 360 + 360) % 360;
66
+ /**
67
+ * The compositor's bearing definition, verbatim: `atan2(sine, cosine)` in
68
+ * degrees, shifted into `[0, 360)`. Exposed so consumers can derive the same
69
+ * bearing the feed colours by, instead of re-deriving it. Mirrors
70
+ * `bufferToDirectionalImageData` (`tile-data.ts`).
71
+ */
72
+ const bearingFromComponents = (sineValue, cosineValue) => {
73
+ let bearingDegrees = Math.atan2(sineValue, cosineValue) * 180 / Math.PI;
74
+ if (bearingDegrees < 0) bearingDegrees += 360;
75
+ return bearingDegrees;
76
+ };
77
+ /**
78
+ * Map a compass bearing to the palette position whose colour the wheel shows
79
+ * there. With `colorAngle === 0` this is `bearing / 360` — exactly how the
80
+ * compositor indexes its gradient (`(bearingDegrees / 360) * RESOLUTION`).
81
+ * `colorAngle` rotates the colour assignment clockwise around the ring.
82
+ */
83
+ const bearingToPalettePosition = (bearing, colorAngle) => normalizeBearing(bearing - colorAngle) / 360;
84
+ /** Point on the wheel for a bearing, using the compass convention (0° at top, clockwise). */
85
+ const polarToCartesian = (centerX, centerY, radius, bearing) => {
86
+ const radians = normalizeBearing(bearing) * TWO_PI / 360;
87
+ return {
88
+ x: centerX + radius * Math.sin(radians),
89
+ y: centerY - radius * Math.cos(radians)
90
+ };
91
+ };
92
+ /** Inclusive/exclusive bearing range covered by sector `index` of `count` equal sectors. */
93
+ const sectorBearingRange = (index, count) => {
94
+ const span = 360 / count;
95
+ return {
96
+ endBearing: (index + 1) * span,
97
+ startBearing: index * span
98
+ };
99
+ };
100
+ /**
101
+ * Stable id for sector `index`, formatted like Horizon's `DEFAULT_DIRECTION_GROUP_LIST`
102
+ * ids (`sector-0-120`) so HZN-2656 can build a `hiddenDirectionGroupList` directly.
103
+ */
104
+ const sectorId = (index, count) => {
105
+ const { endBearing, startBearing } = sectorBearingRange(index, count);
106
+ return `sector-${Math.round(startBearing)}-${Math.round(endBearing)}`;
107
+ };
108
+ /** Threshold (coherence, `0..1`) → CSS `saturate()` multiplier. Higher threshold → more desaturated. */
109
+ const thresholdToSaturation = (threshold) => Math.max(0, Math.min(1, 1 - threshold));
110
+ const arcSampleCount = (sweep) => Math.max(2, Math.ceil(Math.abs(sweep) / 6));
111
+ /**
112
+ * SVG path (`d`) for an annular sector (donut wedge) spanning `[startBearing, endBearing]`
113
+ * between `innerRadius` and `outerRadius`. Used for the divider/dim/highlight overlays.
114
+ */
115
+ const annularSectorPath = (centerX, centerY, innerRadius, outerRadius, startBearing, endBearing) => {
116
+ const sweep = endBearing - startBearing;
117
+ const samples = arcSampleCount(sweep);
118
+ const outer = [];
119
+ const inner = [];
120
+ for (let step = 0; step <= samples; step += 1) {
121
+ const bearing = startBearing + sweep * step / samples;
122
+ const outerPoint = polarToCartesian(centerX, centerY, outerRadius, bearing);
123
+ const innerPoint = polarToCartesian(centerX, centerY, innerRadius, bearing);
124
+ outer.push(`${outerPoint.x.toFixed(3)} ${outerPoint.y.toFixed(3)}`);
125
+ inner.push(`${innerPoint.x.toFixed(3)} ${innerPoint.y.toFixed(3)}`);
126
+ }
127
+ inner.reverse();
128
+ return `M ${outer[0]} L ${outer.slice(1).join(" L ")} L ${inner.join(" L ")} Z`;
129
+ };
130
+ /**
131
+ * CSS `clip-path: polygon(…)` for an annular sector, in percentage coordinates of a
132
+ * square box (centre at `50% 50%`). Lets a full-size, transparent `<button>` expose
133
+ * only its wedge as the interactive/hit region.
134
+ */
135
+ const sectorClipPath = (innerRadiusPercent, outerRadiusPercent, startBearing, endBearing) => {
136
+ const sweep = endBearing - startBearing;
137
+ const samples = arcSampleCount(sweep);
138
+ const outer = [];
139
+ const inner = [];
140
+ for (let step = 0; step <= samples; step += 1) {
141
+ const bearing = startBearing + sweep * step / samples;
142
+ const outerPoint = polarToCartesian(50, 50, outerRadiusPercent, bearing);
143
+ const innerPoint = polarToCartesian(50, 50, innerRadiusPercent, bearing);
144
+ outer.push(`${outerPoint.x.toFixed(3)}% ${outerPoint.y.toFixed(3)}%`);
145
+ inner.push(`${innerPoint.x.toFixed(3)}% ${innerPoint.y.toFixed(3)}%`);
146
+ }
147
+ inner.reverse();
148
+ return `polygon(${[...outer, ...inner].join(", ")})`;
149
+ };
150
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
151
+ const roundTo = (value, digits) => {
152
+ const factor = 10 ** digits;
153
+ return Math.round(value * factor) / factor;
154
+ };
155
+ const OKLCH_PATTERN = /^oklch\(\s*([\d.]+)(%?)\s+([\d.]+)\s+([\d.]+)/i;
156
+ const parseOklch = (color) => {
157
+ const match = OKLCH_PATTERN.exec(color.trim());
158
+ if (match === null) return null;
159
+ const lightnessRaw = Number.parseFloat(match[1]);
160
+ return {
161
+ c: Number.parseFloat(match[3]),
162
+ h: Number.parseFloat(match[4]),
163
+ l: match[2] === "%" ? lightnessRaw / 100 : lightnessRaw
164
+ };
165
+ };
166
+ const formatOklch = (oklch) => `oklch(${roundTo(oklch.l, 4)} ${roundTo(oklch.c, 4)} ${roundTo(oklch.h, 2)})`;
167
+ /**
168
+ * Colour of a palette at a normalized position `[0, 1]`, interpolating OKLCH stops with
169
+ * shortest-hue — the same interpolation the compositor's `buildBearingGradient` uses. Falls
170
+ * back to the nearest stop's raw colour for non-OKLCH palettes (e.g. hsl/hex), which can't be
171
+ * safely lerped without a full colour library.
172
+ */
173
+ const colorForPalettePosition = (stops, position) => {
174
+ const ordered = stops.slice().sort((first, second) => first.position - second.position);
175
+ const first = ordered[0];
176
+ const last = ordered[ordered.length - 1];
177
+ if (first === void 0 || last === void 0) return "transparent";
178
+ const clamped = clamp01(position);
179
+ if (clamped <= first.position) return first.color;
180
+ if (clamped >= last.position) return last.color;
181
+ let lower = first;
182
+ let upper = last;
183
+ for (let index = 0; index < ordered.length - 1; index += 1) {
184
+ const candidate = ordered[index];
185
+ const next = ordered[index + 1];
186
+ if (candidate !== void 0 && next !== void 0 && clamped >= candidate.position && clamped <= next.position) {
187
+ lower = candidate;
188
+ upper = next;
189
+ break;
190
+ }
191
+ }
192
+ const span = upper.position - lower.position;
193
+ const factor = span === 0 ? 0 : (clamped - lower.position) / span;
194
+ const lowerOklch = parseOklch(lower.color);
195
+ const upperOklch = parseOklch(upper.color);
196
+ if (lowerOklch === null || upperOklch === null) return factor < .5 ? lower.color : upper.color;
197
+ let hueDelta = upperOklch.h - lowerOklch.h;
198
+ if (hueDelta > 180) hueDelta -= 360;
199
+ if (hueDelta < -180) hueDelta += 360;
200
+ return formatOklch({
201
+ c: lowerOklch.c + (upperOklch.c - lowerOklch.c) * factor,
202
+ h: (lowerOklch.h + hueDelta * factor + 360) % 360,
203
+ l: lowerOklch.l + (upperOklch.l - lowerOklch.l) * factor
204
+ });
205
+ };
206
+ /**
207
+ * Flat colour for a sector centred at `centerBearing`, honouring `colorAngle`. Each GRAMS
208
+ * sector is filled with one colour — the palette evaluated at
209
+ * `bearingToPalettePosition(centerBearing, colorAngle)`, i.e. the same colour the compositor
210
+ * would give a pixel whose bearing is the sector's centre.
211
+ */
212
+ const resolveSectorColor = (source, centerBearing, colorAngle) => {
213
+ const rotatedBearing = normalizeBearing(centerBearing - colorAngle);
214
+ if (typeof source.colorForBearing === "function") return source.colorForBearing(rotatedBearing);
215
+ return colorForPalettePosition(source.colorStops !== void 0 && source.colorStops !== null && source.colorStops.length > 0 ? source.colorStops : DEFAULT_DIRECTIONAL_COLOR_STOPS, rotatedBearing / 360);
216
+ };
217
+
218
+ //#endregion
219
+ export { DEFAULT_DIRECTIONAL_COLOR_STOPS, SUPPORTED_SECTOR_COUNTS, annularSectorPath, bearingFromComponents, bearingToPalettePosition, colorForPalettePosition, normalizeBearing, polarToCartesian, resolveSectorColor, sectorBearingRange, sectorClipPath, sectorId, thresholdToSaturation };
220
+ //# sourceMappingURL=directionalColorWheelMath.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"directionalColorWheelMath.js","names":[],"sources":["../../src/components/DirectionalColorWheel/directionalColorWheelMath.ts"],"sourcesContent":["/**\n * Pure geometry + colour-mapping helpers for {@link DirectionalColorWheel}.\n *\n * The wheel is a *legend* for the directional spectrogram, not an independent\n * colour scheme. Its bearing→colour convention is intentionally identical to the\n * CPU compositor `bufferToDirectionalImageData` (Horizon `tile-data.ts`). Keeping\n * this in a small, dependency-free module lets it be unit-tested in isolation and\n * gives consumers (HZN-2656) a single importable source for the convention. See\n * `DirectionalColorWheel.consistency.md` for the full alignment proof.\n */\n\n/** Sector counts the wheel supports, matching the GRAMS dialog. */\nexport const SUPPORTED_SECTOR_COUNTS = [4, 8, 12, 16, 20, 24, 32] as const\n\nexport type DirectionalSectorCount = (typeof SUPPORTED_SECTOR_COUNTS)[number]\n\n/** A palette stop, mirroring the shape of Horizon's `DIRECTIONAL_OKLCH_STOP_LIST`. */\nexport interface DirectionalColorStop {\n /** Any CSS colour (an `oklch(…)` string keeps parity with the compositor's palette). */\n color: string\n /** Position along the palette, `0` (bearing 0°) to `1` (bearing 360°). */\n position: number\n}\n\n/**\n * Default palette. These are the same OKLCH stops, at the same positions, as the\n * compositor's `DIRECTIONAL_OKLCH_STOP_LIST` — so a wheel rendered with no\n * `colorStops`/`colorForBearing` prop already matches the directional feed's\n * default DIFAR palette. Consumers override this with the exact feed palette.\n */\nexport const DEFAULT_DIRECTIONAL_COLOR_STOPS: DirectionalColorStop[] = [\n { color: 'oklch(0.65 0.26 29)', position: 0 },\n { color: 'oklch(0.7 0.17 55)', position: 0.083 },\n { color: 'oklch(0.9 0.21 110)', position: 0.167 },\n { color: 'oklch(0.87 0.29 142)', position: 0.333 },\n { color: 'oklch(0.45 0.26 264)', position: 0.556 },\n { color: 'oklch(0.33 0.18 300)', position: 0.722 },\n { color: 'oklch(0.5 0.27 328)', position: 0.889 },\n { color: 'oklch(0.65 0.26 29)', position: 1 },\n]\n\nconst TWO_PI = Math.PI * 2\n\n/** Wrap any angle into `[0, 360)`. */\nexport const normalizeBearing = (degrees: number): number => ((degrees % 360) + 360) % 360\n\n/**\n * The compositor's bearing definition, verbatim: `atan2(sine, cosine)` in\n * degrees, shifted into `[0, 360)`. Exposed so consumers can derive the same\n * bearing the feed colours by, instead of re-deriving it. Mirrors\n * `bufferToDirectionalImageData` (`tile-data.ts`).\n */\nexport const bearingFromComponents = (sineValue: number, cosineValue: number): number => {\n let bearingDegrees = (Math.atan2(sineValue, cosineValue) * 180) / Math.PI\n if (bearingDegrees < 0) {\n bearingDegrees += 360\n }\n return bearingDegrees\n}\n\n/**\n * Map a compass bearing to the palette position whose colour the wheel shows\n * there. With `colorAngle === 0` this is `bearing / 360` — exactly how the\n * compositor indexes its gradient (`(bearingDegrees / 360) * RESOLUTION`).\n * `colorAngle` rotates the colour assignment clockwise around the ring.\n */\nexport const bearingToPalettePosition = (bearing: number, colorAngle: number): number => normalizeBearing(bearing - colorAngle) / 360\n\n/** Point on the wheel for a bearing, using the compass convention (0° at top, clockwise). */\nexport const polarToCartesian = (centerX: number, centerY: number, radius: number, bearing: number): { x: number; y: number } => {\n const radians = (normalizeBearing(bearing) * TWO_PI) / 360\n return {\n x: centerX + radius * Math.sin(radians),\n y: centerY - radius * Math.cos(radians),\n }\n}\n\n/** Inclusive/exclusive bearing range covered by sector `index` of `count` equal sectors. */\nexport const sectorBearingRange = (index: number, count: number): { endBearing: number; startBearing: number } => {\n const span = 360 / count\n return { endBearing: (index + 1) * span, startBearing: index * span }\n}\n\n/**\n * Stable id for sector `index`, formatted like Horizon's `DEFAULT_DIRECTION_GROUP_LIST`\n * ids (`sector-0-120`) so HZN-2656 can build a `hiddenDirectionGroupList` directly.\n */\nexport const sectorId = (index: number, count: number): string => {\n const { endBearing, startBearing } = sectorBearingRange(index, count)\n return `sector-${Math.round(startBearing)}-${Math.round(endBearing)}`\n}\n\n/** Threshold (coherence, `0..1`) → CSS `saturate()` multiplier. Higher threshold → more desaturated. */\nexport const thresholdToSaturation = (threshold: number): number => Math.max(0, Math.min(1, 1 - threshold))\n\nconst arcSampleCount = (sweep: number): number => Math.max(2, Math.ceil(Math.abs(sweep) / 6))\n\n/**\n * SVG path (`d`) for an annular sector (donut wedge) spanning `[startBearing, endBearing]`\n * between `innerRadius` and `outerRadius`. Used for the divider/dim/highlight overlays.\n */\nexport const annularSectorPath = (centerX: number, centerY: number, innerRadius: number, outerRadius: number, startBearing: number, endBearing: number): string => {\n const sweep = endBearing - startBearing\n const samples = arcSampleCount(sweep)\n const outer: string[] = []\n const inner: string[] = []\n for (let step = 0; step <= samples; step += 1) {\n const bearing = startBearing + (sweep * step) / samples\n const outerPoint = polarToCartesian(centerX, centerY, outerRadius, bearing)\n const innerPoint = polarToCartesian(centerX, centerY, innerRadius, bearing)\n outer.push(`${outerPoint.x.toFixed(3)} ${outerPoint.y.toFixed(3)}`)\n inner.push(`${innerPoint.x.toFixed(3)} ${innerPoint.y.toFixed(3)}`)\n }\n inner.reverse()\n return `M ${outer[0]} L ${outer.slice(1).join(' L ')} L ${inner.join(' L ')} Z`\n}\n\n/**\n * CSS `clip-path: polygon(…)` for an annular sector, in percentage coordinates of a\n * square box (centre at `50% 50%`). Lets a full-size, transparent `<button>` expose\n * only its wedge as the interactive/hit region.\n */\nexport const sectorClipPath = (innerRadiusPercent: number, outerRadiusPercent: number, startBearing: number, endBearing: number): string => {\n const sweep = endBearing - startBearing\n const samples = arcSampleCount(sweep)\n const outer: string[] = []\n const inner: string[] = []\n for (let step = 0; step <= samples; step += 1) {\n const bearing = startBearing + (sweep * step) / samples\n const outerPoint = polarToCartesian(50, 50, outerRadiusPercent, bearing)\n const innerPoint = polarToCartesian(50, 50, innerRadiusPercent, bearing)\n outer.push(`${outerPoint.x.toFixed(3)}% ${outerPoint.y.toFixed(3)}%`)\n inner.push(`${innerPoint.x.toFixed(3)}% ${innerPoint.y.toFixed(3)}%`)\n }\n inner.reverse()\n return `polygon(${[...outer, ...inner].join(', ')})`\n}\n\nconst clamp01 = (value: number): number => Math.max(0, Math.min(1, value))\n\nconst roundTo = (value: number, digits: number): number => {\n const factor = 10 ** digits\n return Math.round(value * factor) / factor\n}\n\nconst OKLCH_PATTERN = /^oklch\\(\\s*([\\d.]+)(%?)\\s+([\\d.]+)\\s+([\\d.]+)/i\n\nconst parseOklch = (color: string): { c: number; h: number; l: number } | null => {\n const match = OKLCH_PATTERN.exec(color.trim())\n if (match === null) return null\n const lightnessRaw = Number.parseFloat(match[1])\n return { c: Number.parseFloat(match[3]), h: Number.parseFloat(match[4]), l: match[2] === '%' ? lightnessRaw / 100 : lightnessRaw }\n}\n\nconst formatOklch = (oklch: { c: number; h: number; l: number }): string => `oklch(${roundTo(oklch.l, 4)} ${roundTo(oklch.c, 4)} ${roundTo(oklch.h, 2)})`\n\n/**\n * Colour of a palette at a normalized position `[0, 1]`, interpolating OKLCH stops with\n * shortest-hue — the same interpolation the compositor's `buildBearingGradient` uses. Falls\n * back to the nearest stop's raw colour for non-OKLCH palettes (e.g. hsl/hex), which can't be\n * safely lerped without a full colour library.\n */\nexport const colorForPalettePosition = (stops: DirectionalColorStop[], position: number): string => {\n const ordered = stops.slice().sort((first, second) => first.position - second.position)\n const first = ordered[0]\n const last = ordered[ordered.length - 1]\n if (first === undefined || last === undefined) return 'transparent'\n const clamped = clamp01(position)\n if (clamped <= first.position) return first.color\n if (clamped >= last.position) return last.color\n\n let lower = first\n let upper = last\n for (let index = 0; index < ordered.length - 1; index += 1) {\n const candidate = ordered[index]\n const next = ordered[index + 1]\n if (candidate !== undefined && next !== undefined && clamped >= candidate.position && clamped <= next.position) {\n lower = candidate\n upper = next\n break\n }\n }\n\n const span = upper.position - lower.position\n const factor = span === 0 ? 0 : (clamped - lower.position) / span\n const lowerOklch = parseOklch(lower.color)\n const upperOklch = parseOklch(upper.color)\n if (lowerOklch === null || upperOklch === null) return factor < 0.5 ? lower.color : upper.color\n\n let hueDelta = upperOklch.h - lowerOklch.h\n if (hueDelta > 180) hueDelta -= 360\n if (hueDelta < -180) hueDelta += 360\n return formatOklch({\n c: lowerOklch.c + (upperOklch.c - lowerOklch.c) * factor,\n h: (lowerOklch.h + hueDelta * factor + 360) % 360,\n l: lowerOklch.l + (upperOklch.l - lowerOklch.l) * factor,\n })\n}\n\n/**\n * Flat colour for a sector centred at `centerBearing`, honouring `colorAngle`. Each GRAMS\n * sector is filled with one colour — the palette evaluated at\n * `bearingToPalettePosition(centerBearing, colorAngle)`, i.e. the same colour the compositor\n * would give a pixel whose bearing is the sector's centre.\n */\nexport const resolveSectorColor = (source: { colorForBearing?: ((bearing: number) => string) | null; colorStops?: DirectionalColorStop[] | null }, centerBearing: number, colorAngle: number): string => {\n const rotatedBearing = normalizeBearing(centerBearing - colorAngle)\n if (typeof source.colorForBearing === 'function') return source.colorForBearing(rotatedBearing)\n const stops = source.colorStops !== undefined && source.colorStops !== null && source.colorStops.length > 0 ? source.colorStops : DEFAULT_DIRECTIONAL_COLOR_STOPS\n return colorForPalettePosition(stops, rotatedBearing / 360)\n}\n"],"mappings":"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"}
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+ import { DirectionalColorStop, DirectionalSectorCount } from "./DirectionalColorWheel/directionalColorWheelMath.js";
3
+ import { Ref } from "react";
4
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
5
+
6
+ //#region src/components/DirectionalColorWheel/DirectionalColorWheel.d.ts
7
+ interface DirectionalColorWheelProps {
8
+ /** Accessible name for the wheel group. */
9
+ accessibleName?: string;
10
+ /**
11
+ * Read-only bearing (degrees) the needle points at, using the compositor's `atan2(sine, cosine)`
12
+ * convention (0° = N, clockwise). The consumer drives it (e.g. from the feed hover cursor) so the
13
+ * needle follows the cursor; the wheel only displays it — there is no drag interaction.
14
+ * `null`/`undefined` hides the needle — keep it nullable so Horizon can gate the feature flag with
15
+ * a single `null` (PAT-028).
16
+ */
17
+ bearing?: number | null;
18
+ /** Layout-only class extension (margins/padding). */
19
+ className?: string;
20
+ /**
21
+ * "Color Angle": degrees the colours are rotated clockwise around the ring. Controlled+uncontrolled
22
+ * (pair it with `defaultColorAngle` / `onColorAngleChange`). The colours can also be spun directly by
23
+ * dragging the colour ring — that gesture reports through `onColorAngleChange` and is only active when
24
+ * the value can take effect (uncontrolled, or controlled *with* an `onColorAngleChange` handler). Sector
25
+ * geometry and the N/E/S/W bezel stay fixed to compass bearings; only the colours rotate.
26
+ */
27
+ colorAngle?: number;
28
+ /**
29
+ * Advanced parity hook: resolver mapping a bearing in `[0, 360)` to a CSS colour. When
30
+ * provided it wins over `colorStops`; pass the same resolver the feed colours by so the
31
+ * legend samples the identical colours.
32
+ */
33
+ colorForBearing?: ((bearingDegrees: number) => string) | null;
34
+ /** Palette stops (position `0..1`). Defaults to the directional DIFAR palette. */
35
+ colorStops?: DirectionalColorStop[] | null;
36
+ dataTestId?: string;
37
+ /** Uncontrolled initial Color Angle (degrees). */
38
+ defaultColorAngle?: number;
39
+ /** Uncontrolled initial set of disabled (hidden) sector indices. */
40
+ defaultDisabledSectors?: number[];
41
+ /** Uncontrolled initial threshold. */
42
+ defaultThreshold?: number;
43
+ disabled?: boolean;
44
+ /** Controlled set of disabled (hidden) sector indices, in `[0, sectorCount)`. */
45
+ disabledSectors?: number[] | null;
46
+ /** Called with the next Color Angle (degrees, `[0, 360)`) as the colour ring is dragged. */
47
+ onColorAngleChange?: (colorAngle: number) => void;
48
+ /** Called with the next set of disabled sector indices whenever a wedge is toggled. */
49
+ onDisabledSectorsChange?: (disabledSectors: number[]) => void;
50
+ /** Called when a single wedge is toggled: its index and whether it is now disabled. */
51
+ onSectorToggle?: (sectorIndex: number, nextDisabled: boolean) => void;
52
+ /** Called with the threshold (`0..1`) as the centre dial changes. */
53
+ onThresholdChange?: (threshold: number) => void;
54
+ ref?: Ref<HTMLDivElement>;
55
+ /** Number of toggleable wedges. */
56
+ sectorCount?: DirectionalSectorCount;
57
+ /** Diameter in pixels. */
58
+ size?: number;
59
+ /**
60
+ * Coherence threshold (`0..1`). Coherence is how reliable the bearing estimate is; this dial
61
+ * maps it to saturation — higher threshold desaturates the legend. The component only reports
62
+ * the value via `onThresholdChange`; the consumer decides what it drives.
63
+ */
64
+ threshold?: number;
65
+ /** Keyboard step for the threshold dial. */
66
+ thresholdStep?: number;
67
+ }
68
+ declare function DirectionalColorWheel({
69
+ accessibleName,
70
+ bearing,
71
+ className,
72
+ colorAngle,
73
+ colorForBearing,
74
+ colorStops,
75
+ dataTestId,
76
+ defaultColorAngle,
77
+ defaultDisabledSectors,
78
+ defaultThreshold,
79
+ disabled,
80
+ disabledSectors,
81
+ onColorAngleChange,
82
+ onDisabledSectorsChange,
83
+ onSectorToggle,
84
+ onThresholdChange,
85
+ ref,
86
+ sectorCount,
87
+ size,
88
+ threshold,
89
+ thresholdStep
90
+ }: DirectionalColorWheelProps): _$react_jsx_runtime0.JSX.Element;
91
+ declare namespace DirectionalColorWheel {
92
+ var displayName: string;
93
+ }
94
+ //#endregion
95
+ export { DirectionalColorWheel, DirectionalColorWheelProps };
96
+ //# sourceMappingURL=DirectionalColorWheel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DirectionalColorWheel.d.ts","names":[],"sources":["../src/components/DirectionalColorWheel/DirectionalColorWheel.tsx"],"mappings":";;;;;;UAKiB,0BAAA;;EAEf,cAAA;EAFe;;;;;;;EAUf,OAAA;EAuCoC;EArCpC,SAAA;EAFA;;;;;;;EAUA,UAAA;EAWA;;;;;EALA,eAAA,KAAoB,cAAA;EAcE;EAZtB,UAAA,GAAa,oBAAA;EACb,UAAA;EAeA;EAbA,iBAAA;EAauC;EAXvC,sBAAA;EAaqB;EAXrB,gBAAA;EACA,QAAA;EAWU;EATV,eAAA;EAWc;EATd,kBAAA,IAAsB,UAAA;EAiBtB;EAfA,uBAAA,IAA2B,eAAA;EAiBd;EAfb,cAAA,IAAkB,WAAA,UAAqB,YAAA;;EAEvC,iBAAA,IAAqB,SAAA;EACrB,GAAA,GAAM,GAAA,CAAI,cAAA;EA+FV;EA7FA,WAAA,GAAc,sBAAA;EA+Fd;EA7FA,IAAA;EA+FA;;;;;EAzFA,SAAA;EA+FA;EA7FA,aAAA;AAAA;AAAA;EAmFA,cAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,eAAA;EACA,UAAA;EACA,UAAA;EACA,iBAAA;EACA,sBAAA;EACA,gBAAA;EACA,QAAA;EACA,eAAA;EACA,kBAAA;EACA,uBAAA;EACA,cAAA;EACA,iBAAA;EACA,GAAA;EACA,WAAA;EACA,IAAA;EACA,SAAA;EACA;AAAA,GACC,0BAAA,GAA0B,oBAAA,CAAA,GAAA,CAAA,OAAA;AAAA"}