import type { MdyTimeFormat, ParsedTime } from "@modyra/core/datetime"; import { type MdyTimeSteps } from "../time-granularity.js"; export interface MdyTimepickerDialNumber { readonly value: number; readonly label: string; readonly index: number; /** * Which ring the number sits on. A face has twelve positions, and 24-hour time has twenty-four * hours, so the second twelve go on an inner ring at the same twelve positions — which is what a * clock has always done and what Material's own 24-hour dial does. * * `"outer"` for every number on a 12-hour face and for 1–12 on a 24-hour one; `"inner"` for * 13–23 and for midnight. */ readonly ring: "outer" | "inner"; } /** * The numbers on the face for the field being picked. * * The hours a face offers are the hours the format has: 1–12 with an AM/PM toggle beside them, or * 0–23 with no toggle at all. It used to always answer 1–12, so a 24-hour picker showed a face on * which 14:00 could not be pointed at — the value was reachable by typing and by dragging, and not * by the control that exists to pick it. **A time keeps its formalism on screen as well as in its * value**: a face that offers thirteen hours in a twelve-hour clock, or twelve in a twenty-four * hour one, is telling the user something untrue about what they are editing. */ export declare function timepickerDialNumbers(field: "hour" | "minute", format?: MdyTimeFormat, steps?: MdyTimeSteps): readonly MdyTimepickerDialNumber[]; /** Where a number sits, in degrees clockwise from the top. */ export declare function dialNumberAngle(number: MdyTimepickerDialNumber): number; /** * Where a dragged pointer landed: the value, the angle it sits at, and which ring it is on. * * The angle is the **number's** angle, not the pointer's, so a renderer rests the hand on what was * chosen rather than under the finger. The ring is what lets it draw a shorter hand for the inner * one — at `hourStep: 3` the outer 3 and the inner 15 share a position, and without that difference * the two selections are indistinguishable on screen. */ export interface MdyTimepickerDialPick { readonly value: number; readonly angle: number; readonly ring: "outer" | "inner"; } /** * The value a dragged pointer lands on: the nearest one the field **offers**. * * Offered is not the same set as drawn, and conflating them coarsens the dial. A minute face has * twelve positions and a minute field has sixty values, so an ungranulated picker draws 0, 5, 10 … * and still accepts every minute — the hand sits between two labels, which is what a clock does. * Landing only on drawn numbers would silently turn every picker in the library into a five-minute * one. * * What must hold instead is that a drag can only reach a value the field would accept, at any step. * Hours have as many values as positions, so for them the two sets coincide; minutes do not. * * A tie — an angle exactly between two offered values — goes **clockwise**, to the later of the two. * Arbitrary, but stated: an unstated tie-break is one the face and the keyboard can resolve * differently. * * Returns `null` only when the granularity offers this field nothing at all, which a validated one * never does. */ export declare function timepickerDialPick(angle: number, field: "hour" | "minute", format?: MdyTimeFormat, ring?: "outer" | "inner", steps?: MdyTimeSteps, previous?: number): MdyTimepickerDialPick | null; /** * Which number on the face is the selected one. * * Minutes are shown in fives, so a draft of 07 marks the 05 that is nearest to it rather than * marking nothing at all. * * **In the units the face shows.** The draft holds hours as 1–12 with a period whatever the format, * and a 24-hour face is numbered 0–23 — so answering the draft's hour marked `2` while the user was * looking at `14`. The face's numbers and the face's mark have to be decided by the same rule, or * they will eventually disagree; this is that rule, and `timepickerDialNumbers` is its other half. */ export declare function timepickerSelectedDialValue(field: "hour" | "minute", draft: ParsedTime, format?: MdyTimeFormat): number; /** * The second hand: where the pointer is, when that is not where the value is. * * A dial that offers only some times has to snap, and snapping alone hides what it is doing — the * hand jumps to a number the finger is not on, and whether that was the rule or a missed press is * not something the screen says. So the chosen value keeps the hand it always had, and a translucent * one follows the pointer whenever the two are apart. The gap between them **is** the explanation. * * Nothing to show when they agree, which is every picker that offers every time: a ghost permanently * under the real hand would be a second thing to look at that never means anything. * * `within` is the tolerance in degrees, and it is the contract's rather than a renderer's for the * usual reason — three answers to "close enough" is three widgets. Half the angular gap between two * offered values is what it defaults to: the ghost appears exactly when the pointer is nearer some * other number than the one that was taken. */ export interface MdyTimepickerDialGhost { readonly angle: number; readonly ring: "outer" | "inner"; /** * How long to draw it, as a fraction of the hand. * * **The end is under the pointer**, which is the whole of its meaning — a ghost that stopped short * would be reporting a position nobody is at. So this is the pointer's own distance from the * centre, with one exception: it is capped at the hand's length, because past that the face runs * out and a longer hand would spill over its own numbers. * * There is deliberately **no floor**. A finger 15px from the centre gets a 15px stub, which looks * like nothing much and is exactly right: that is where the finger is. */ readonly reach: number; } export declare function timepickerDialGhost(pointerAngle: number, pick: MdyTimepickerDialPick, options?: { readonly ring?: "outer" | "inner"; readonly within?: number; /** How far the pointer is from the centre, and how long the hand is, in the same units. */ readonly pointerReach?: number; readonly handLength?: number; }): MdyTimepickerDialGhost | null; /** * How far off a number the pointer can be and still be **on** it, in degrees. * * Not half the gap to the next number. That was the first answer and it is a tautology: the pick is * *defined* as the nearest offered value, so the pointer is always within half a gap of it, and a * ghost that appears past half a gap never appears at all. Measured across three faces it was hidden * at every one of 360 angles, with every unit test of the parts still green. * * On the number means on the number. A digit is `MDY_TIMEPICKER_NUMBER_SIZE` wide, drawn at a radius * of `handLength` outside and `handLength × MDY_TIMEPICKER_INNER_RING` inside, so it subtends * `atan((size / 2) / radius)` either side of its own direction — about 11° out there and about 18° * further in, because a same-sized number covers more of a smaller circle. */ export declare function timepickerDialTolerance(ring?: "outer" | "inner", handLength?: number): number; /** A stretch of the circle, clockwise from `from` for `span` degrees, on one of the face's rings. */ export interface MdyTimepickerDialArc { readonly from: number; readonly to: number; /** How far it runs, so a renderer does not do modular arithmetic to draw a wedge. */ readonly span: number; readonly ring: "outer" | "inner"; } /** * The stretches of a ring that carry no time anybody can land on. * * A face declared with `minuteStep: 15` draws four numbers, and the other 356° of the ring look * exactly like the four — continuous, uniform, and offering nothing. The granularity is real and * invisible, discoverable only by trying, which is the state a dimmed arc is meant to end. * * The arcs are the positions the granularity **took away** — the ones a face with no declaration * would draw and this one does not — each covered by the knob's own angular half-width, which is * already the tolerance for whether a pointer is on a number. Not the space between the numbers that * remain: a face offering every hour has gaps between its knobs and nothing unavailable in them. * * **Two widths, deliberately.** Snapping is still nearest-value: every angle resolves to some number, * including the ones inside these arcs. What is dimmed is where you can *land on the number you are * pointing at*, which is a narrower question and a display one. The next reader will want to make the * two agree; they are different questions and the answer is no. * * A face whose knobs already overlap has nothing to dim and answers `[]` — sixty minutes 6° apart * under an 11° half-width is the ordinary case, so a ring of slivers there would be noise on every * picker that declares nothing. */ export declare function timepickerDialUnavailableArcs(field: "hour" | "minute", format?: MdyTimeFormat, steps?: MdyTimeSteps, handLength?: number, ring?: "outer" | "inner"): readonly MdyTimepickerDialArc[]; /** * Which ring a value sits on. One predicate, so the face and the hand cannot disagree about it. * * Only a 24-hour face has two rings: midnight and 13–23 go inside, at the same twelve positions as * 12 and 1–11. */ export declare function dialRingOf(field: "hour" | "minute", value: number, format: MdyTimeFormat): "outer" | "inner"; /** * Which ring the hand is currently pointing into. * * The hand is drawn shorter for the inner ring, and that is not decoration: a granularity can put * two hours at one position — with `hourStep: 3` the outer 3 and the inner 15 share a direction — * and without the difference in length the two selections are identical on screen. A person cannot * tell which they chose until they read the header. */ export declare function timepickerSelectedRing(field: "hour" | "minute", draft: ParsedTime, format?: MdyTimeFormat): "outer" | "inner"; /** What a key does to the number the dial is pointing at. */ export interface MdyTimepickerDialKeyResult { /** The value the field takes, in the units the face shows. */ readonly value: number; /** Which field it belongs to, so a host applies it without re-deriving anything. */ readonly field: "hour" | "minute"; } /** * The keyboard on the clock face. * * A dial is a control and had none: it listened for `mousedown` and `touchstart` and nothing else, * so the clock could only be operated by pointer. Everything on it was reachable by *dragging* a * hand around a circle, which is the one gesture a keyboard cannot make and a screen reader cannot * describe. * * The policy, once, here: * * - **Right/Up** steps forward, **Left/Down** back. Up is forward because the hand sweeps clockwise; * a keyboard user turning the hand should feel the same direction the pointer drags. * - **Home/End** go to the first and last value the face offers — which depends on the format, and * is exactly the rule this function exists to hold in one place. * - **PageUp/PageDown** move minutes by the five the face is marked in, and hours by three, so a * quarter of the dial is one keystroke. * - Every result **wraps** rather than clamping. A clock is a ring: 23 then 00, 59 then 00, 12 then * 1. Clamping at the end of a circle is the one behaviour a dial cannot justify. * * And the range is the format's: hours run **1–12 with a period beside them, or 0–23 with none**. * Never thirteen hours on a twelve-hour clock, never a stray AM on a twenty-four hour one. * * Returns `null` for a key the dial does not claim, so a host can let it through. */ /** * How far in the inner ring sits, as a fraction of the **hand's length**. * * The stylesheet draws it at `0.6` of `--tp-hand-length` and says why there; this is the same number * because the two have to agree. `INNER_RING_RADIUS` is the one value shared between the drawing and * the hit test, and a contract test holds it against the stylesheet — everything else about where * the rings land is measured rather than assumed. */ export declare const MDY_TIMEPICKER_INNER_RING = 0.6; /** * How wide a number on the face is, in pixels. * * The stylesheet's `--tp-num-size`, which is also what `--tp-hand-length` subtracts half of to find * the radius the outer digits sit at. Published here for the same reason the ring fraction is: the * question "is the pointer on this number" is answered in the contract, and a renderer measuring the * box itself would be a second answer that drifts. `css-properties.spec.mjs` holds the two together. */ export declare const MDY_TIMEPICKER_NUMBER_SIZE = 40; /** * How far **above** the inner ring's radius still counts as reaching for it, as a fraction of the * gap between the two painted radii. * * `0.5` is not a tuning. A digit box is `MDY_TIMEPICKER_NUMBER_SIZE` wide and the two rings are * exactly that far apart, so the boxes touch: the inner one spans 40–80 and the outer 80–120 at a * hand of 100. *"Il puntatore al centro tra la fine del box del carattere 9 e la fine del box del * carattere 21"* has one answer, the single point where they meet, and half the gap is its closed * form. * * It was `0.35` for a while, and that number was compensating for a measurement: `--tp-hand-length` * was being read back as an unresolved `calc()` and every renderer fell through to half the *face*, * 128 where the hand is 100. Against that circle the edge landed near 95 — a press just inside the * outer digits read as the inner ring — and tightening the fraction moved it back by hand. With the * hand measured properly the construction gives 80 on its own, and 0.35 would leave 74–80 answering * `outer` while the pointer is inside the inner digit's own box: the same complaint mirrored. * * A fraction rather than an expression, still, because where the edge belongs inside a *touching* * pair is a judgement — this one happens to have been made by the person using it. * * One-sided, and it will look asymmetric: **below the inner ring there is no other ring to belong * to.** The outer ring is outside the inner one, so everything beneath the inner digits is nearer * them than anything else on the face and has exactly one plausible answer — *"se il puntatore va * troppo verso il centro si riscala alla circonferenza maggiore, questo è un grossissimo errore"*. */ export declare const MDY_TIMEPICKER_RING_BAND = 0.5; /** * Which ring of the face a pointer landed on. * * A 12-hour face has one ring and always answers `"outer"`. A 24-hour face has two at the same twelve * positions, so the angle alone cannot name the hour — asked without this, half the numbers the face * draws had no way to be chosen. * * `handLength` is the radius the outer digits are drawn at, which the stylesheet computes as * `dialSize / 2 − numSize / 2 − 8px` and publishes as `--tp-hand-length`. It is passed in rather * than derived because those three numbers belong to the drawing: a copy of them here is a copy that * drifts, and the first version of this function compared a fraction of the *hand* against a * fraction of the *dial radius* — two different lengths — which put the boundary 2.4px outside the * outer digits. Every point on the face read as `inner`, including the outer numbers themselves. * * The inner ring is a **band around where its numbers are drawn**, not everything closer than the * outer ones. Reading the whole disc below the midpoint as inner makes the empty middle of the face * — most of its area — answer with an hour whose number is nowhere near the pointer, and the hand * jumps short for a press the user aimed at the outer ring. The band is as wide as the gap between * the two painted radii, centred on the inner one, so a press has to be near the digits to claim * them and anything else belongs to the ring that is actually drawn out there. * * `field` is here because only the hour has two rings. A minute face draws one, and a rule that * answered `"inner"` for a press near the centre of it shortened the hand for a ring that does not * exist. */ /** * How long this face's hand is at full reach, in pixels, measured from the drawing. * * Every angle-at-a-radius on the dial is a fraction of this, so it has to come from the paint rather * than from a copy of the stylesheet's arithmetic in TypeScript. Two ways of reading it have already * been wrong: * * - **`--tp-hand-length` off the face.** A custom property resolves at *use*, so reading it back * gives the token stream — `calc(256px/2 - 40px/2 - 8px)` — and the fallback beside it ran instead: * half the face, 128 where the hand is 100. * - **The hand's own height, taken as it is.** The hand is drawn shorter when it points into the * inner ring, and which ring it points into is *the answer this measurement produces*. Each state * became the other's cause: on the inner ring it measured 60, whose thresholds put the same * position on the outer ring, whose hand measures 100, whose thresholds put it back. It alternated * on every pointer event — not a tremor, a feedback loop, and no amount of hysteresis damps it * because what moves is the thresholds themselves. * * So the shortened state is divided back out. The modifier the element carries says which state it is * in, and `MDY_TIMEPICKER_INNER_RING` is the fraction the sheet drew it at — the same constant, held * against the stylesheet by `css-properties.spec.mjs`. * * One helper rather than the same line in three renderers: the identical line in three files is how * this arrived, and the next adapter would copy whatever it found. */ export declare function dialHandLength(face: Element): number; export declare function timepickerDialRing(face: { readonly width: number; readonly height: number; readonly left: number; readonly top: number; }, clientX: number, clientY: number, format: MdyTimeFormat, handLength: number, field?: "hour" | "minute", previous?: "outer" | "inner"): "outer" | "inner"; export declare function timepickerDialKeyIntent(key: string, field: "hour" | "minute", format: MdyTimeFormat, current: number, steps?: MdyTimeSteps): MdyTimepickerDialKeyResult | null; /** * What a screen reader is told about the number a person is editing. * * The hour and the minute are the operable controls: they hold the value, they answer the arrows, * and they are what `timepickerTabOrder` walks. The dial beside them sets nothing they cannot set, * so it is hidden and this is where the value is announced — once, on the control that can act on it. * * `valueText` is the reason the three numbers are not enough on their own: `15` on a twelve-hour * face is `3 PM`, and a minute of `5` is `05`. A reader given the raw number says something the face * does not show. */ export declare function timepickerSegmentAria(field: "hour" | "minute", format: MdyTimeFormat, current: number, period?: "AM" | "PM" | null): { readonly role: "spinbutton"; readonly valueMin: number; readonly valueMax: number; readonly valueNow: number; readonly valueText: string; }; /** Semantic state of a timepicker field widget. */