@celestia-island/hikari 0.55.281 → 0.55.283
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/HkSettingsDialog.test.tsx +169 -0
- package/src/components/HkSettingsDialog.tsx +129 -2
- package/src/components/HkSolarLocationSetting.scss +63 -2
- package/src/components/HkSolarLocationSetting.test.tsx +260 -21
- package/src/components/HkSolarLocationSetting.tsx +263 -31
- package/src/components/HkTimeZoneGlobe.tsx +8 -10
- package/src/i18n/locales/ar/platform.json +18 -2
- package/src/i18n/locales/de/platform.json +18 -2
- package/src/i18n/locales/en/platform.json +19 -3
- package/src/i18n/locales/es/platform.json +18 -2
- package/src/i18n/locales/fr/platform.json +19 -3
- package/src/i18n/locales/ja/platform.json +18 -2
- package/src/i18n/locales/ko/platform.json +18 -2
- package/src/i18n/locales/pt/platform.json +18 -2
- package/src/i18n/locales/ru/platform.json +18 -2
- package/src/i18n/locales/zh-Hans/platform.json +19 -3
- package/src/i18n/locales/zh-Hant/platform.json +19 -3
- package/src/index.ts +1 -0
- package/src/theme/solarTimeZones.ts +16 -0
- package/src/theme/useSolarTime.test.ts +153 -0
- package/src/theme/useSolarTime.ts +135 -8
package/package.json
CHANGED
|
@@ -3,12 +3,15 @@ import { readFileSync } from "node:fs";
|
|
|
3
3
|
import { createApp, defineComponent, h, nextTick, ref } from "vue";
|
|
4
4
|
import { Languages, Globe } from "lucide-vue-next";
|
|
5
5
|
import {
|
|
6
|
+
SHEET_MORPH_MS,
|
|
7
|
+
sheetMorphMs,
|
|
6
8
|
HkSettingsBody as HSettingsBody,
|
|
7
9
|
HkSettingsDialog as HSettingsDialog,
|
|
8
10
|
HkSettingsGroup as HSettingsGroup,
|
|
9
11
|
HkSettingsHint as HSettingsHint,
|
|
10
12
|
HkSettingsSub as HSettingsSub,
|
|
11
13
|
} from "./HkSettingsDialog";
|
|
14
|
+
import { STEPFLOW_SWAP_EVENT } from "./HkStepFlow";
|
|
12
15
|
|
|
13
16
|
function mount(component: ReturnType<typeof defineComponent>) {
|
|
14
17
|
const container = document.createElement("div");
|
|
@@ -524,3 +527,169 @@ describe("phone shell (rail → top tab strip, 2026-10-28 user direction)", () =
|
|
|
524
527
|
expect(phoneBlock).toContain("font-size: var(--text-base);");
|
|
525
528
|
});
|
|
526
529
|
});
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Docked-sheet height hand-off (2026-11-04 chest phone report).
|
|
533
|
+
*
|
|
534
|
+
* Inside a phone sheet the modal sizes the window from this shell's
|
|
535
|
+
* content, but it observes `.hk-modal-body-inner` — pinned at
|
|
536
|
+
* `height: 100%` by the host rule — so a section swap never changes the
|
|
537
|
+
* observed box and the sheet kept the height of whichever section it
|
|
538
|
+
* OPENED with (the About card clipped inside a ~24px pane). The shell
|
|
539
|
+
* therefore forwards the pane's settled swap through the family's
|
|
540
|
+
* `hk-stepflow-swap` protocol, which HkModal already turns into a frame
|
|
541
|
+
* re-measure.
|
|
542
|
+
*
|
|
543
|
+
* The morph container only emits `swap` once its ResizeObserver has seen
|
|
544
|
+
* the incoming stage and its settle timer has run, so these tests drive
|
|
545
|
+
* the observer directly (happy-dom ships no layout engine: every rect
|
|
546
|
+
* would read zero, which the container correctly reads as "same box" and
|
|
547
|
+
* never settles).
|
|
548
|
+
*/
|
|
549
|
+
class StageResizeObserver implements ResizeObserver {
|
|
550
|
+
static all: StageResizeObserver[] = [];
|
|
551
|
+
observed: Element[] = [];
|
|
552
|
+
private readonly callback: ResizeObserverCallback;
|
|
553
|
+
constructor(callback: ResizeObserverCallback) {
|
|
554
|
+
this.callback = callback;
|
|
555
|
+
StageResizeObserver.all.push(this);
|
|
556
|
+
}
|
|
557
|
+
observe(target: Element): void {
|
|
558
|
+
this.observed.push(target);
|
|
559
|
+
}
|
|
560
|
+
unobserve(): void {}
|
|
561
|
+
disconnect(): void {}
|
|
562
|
+
/** Fire the callback for the observer watching the transition stage. */
|
|
563
|
+
static settleStage(height = 220): boolean {
|
|
564
|
+
const ro = StageResizeObserver.all.find((o) =>
|
|
565
|
+
o.observed.some((el) => el.classList.contains("hk-tc-stage")),
|
|
566
|
+
);
|
|
567
|
+
if (!ro) return false;
|
|
568
|
+
// Only `contentRect` is consumed by the container; the rest of the
|
|
569
|
+
// entry descriptor is layout detail happy-dom cannot produce.
|
|
570
|
+
const entry = { contentRect: { width: 320, height } } as ResizeObserverEntry;
|
|
571
|
+
ro.callback([entry], ro);
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
describe("HSettingsBody — docked-sheet height hand-off", () => {
|
|
577
|
+
const originalRO = globalThis.ResizeObserver;
|
|
578
|
+
const originalMatchMedia = window.matchMedia;
|
|
579
|
+
|
|
580
|
+
afterEach(() => {
|
|
581
|
+
globalThis.ResizeObserver = originalRO;
|
|
582
|
+
window.matchMedia = originalMatchMedia;
|
|
583
|
+
StageResizeObserver.all = [];
|
|
584
|
+
document.documentElement.removeAttribute("data-css-animations");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
function mountWithSwapLog() {
|
|
588
|
+
StageResizeObserver.all = [];
|
|
589
|
+
globalThis.ResizeObserver = StageResizeObserver;
|
|
590
|
+
const events: Array<{ phase?: string; durationMs?: number }> = [];
|
|
591
|
+
const container = document.createElement("div");
|
|
592
|
+
document.body.appendChild(container);
|
|
593
|
+
container.addEventListener(STEPFLOW_SWAP_EVENT, (e) => {
|
|
594
|
+
events.push((e as CustomEvent).detail ?? {});
|
|
595
|
+
});
|
|
596
|
+
const app = createApp(Host);
|
|
597
|
+
app.mount(container);
|
|
598
|
+
return { container, events, unmount: () => { app.unmount(); container.remove(); } };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function switchToNetwork(container: HTMLElement) {
|
|
602
|
+
container.querySelectorAll<HTMLButtonElement>(".hk-settings__rail-item")[1].click();
|
|
603
|
+
await nextTick();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
it("forwards a settled section swap as a morph beat for the modal", async () => {
|
|
607
|
+
const { container, events, unmount } = mountWithSwapLog();
|
|
608
|
+
await switchToNetwork(container);
|
|
609
|
+
expect(StageResizeObserver.settleStage(220)).toBe(true);
|
|
610
|
+
// The container arms its settle timer for the choreography remainder
|
|
611
|
+
// (300ms default) plus a 40ms grace; give it room.
|
|
612
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
613
|
+
expect(events).toEqual([{ phase: "morph", durationMs: 300 }]);
|
|
614
|
+
unmount();
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("announces an instant settle as soon as the pane height is final", async () => {
|
|
618
|
+
// Growing swap: the stage has already reached its incoming height when
|
|
619
|
+
// the container settles, so the sheet must be told on the first frame —
|
|
620
|
+
// not after the bounded wait (which exists only for the leaving
|
|
621
|
+
// generation still holding a shrink at the old height).
|
|
622
|
+
window.matchMedia = (query: string): MediaQueryList => ({
|
|
623
|
+
matches: query.includes("prefers-reduced-motion"),
|
|
624
|
+
media: query,
|
|
625
|
+
onchange: null,
|
|
626
|
+
addListener: () => undefined,
|
|
627
|
+
removeListener: () => undefined,
|
|
628
|
+
addEventListener: () => undefined,
|
|
629
|
+
removeEventListener: () => undefined,
|
|
630
|
+
dispatchEvent: () => false,
|
|
631
|
+
});
|
|
632
|
+
const { container, events, unmount } = mountWithSwapLog();
|
|
633
|
+
const host = container.querySelector(".hk-tc") as HTMLElement;
|
|
634
|
+
let height = 100;
|
|
635
|
+
Object.defineProperty(host, "offsetHeight", {
|
|
636
|
+
configurable: true,
|
|
637
|
+
get: () => height,
|
|
638
|
+
});
|
|
639
|
+
await switchToNetwork(container); // records the pre-swap height (100)
|
|
640
|
+
height = 150; // the incoming section is taller
|
|
641
|
+
expect(StageResizeObserver.settleStage(220)).toBe(true);
|
|
642
|
+
await new Promise((r) => setTimeout(r, 120));
|
|
643
|
+
expect(events).toEqual([{ phase: "instant" }]);
|
|
644
|
+
unmount();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("snaps the sheet when the swap settles with no animation", async () => {
|
|
648
|
+
// Reduced motion zeroes the container's morph duration.
|
|
649
|
+
window.matchMedia = (query: string): MediaQueryList => ({
|
|
650
|
+
matches: query.includes("prefers-reduced-motion"),
|
|
651
|
+
media: query,
|
|
652
|
+
onchange: null,
|
|
653
|
+
addListener: () => undefined,
|
|
654
|
+
removeListener: () => undefined,
|
|
655
|
+
addEventListener: () => undefined,
|
|
656
|
+
removeEventListener: () => undefined,
|
|
657
|
+
dispatchEvent: () => false,
|
|
658
|
+
});
|
|
659
|
+
const { container, events, unmount } = mountWithSwapLog();
|
|
660
|
+
const host = container.querySelector(".hk-tc") as HTMLElement;
|
|
661
|
+
Object.defineProperty(host, "offsetHeight", {
|
|
662
|
+
configurable: true,
|
|
663
|
+
get: () => 150,
|
|
664
|
+
});
|
|
665
|
+
await switchToNetwork(container);
|
|
666
|
+
expect(StageResizeObserver.settleStage(220)).toBe(true);
|
|
667
|
+
// No layout in happy-dom: the stage never changes, so the instant
|
|
668
|
+
// branch must WAIT out its bounded settle window (a shrink keeps the
|
|
669
|
+
// leaving generation in the flow) — nothing may be announced early…
|
|
670
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
671
|
+
expect(events).toEqual([]);
|
|
672
|
+
// …and the bounded window must still fire (a swap that never changes
|
|
673
|
+
// height must not swallow the announcement).
|
|
674
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
675
|
+
expect(events).toEqual([{ phase: "instant" }]);
|
|
676
|
+
unmount();
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
describe("sheetMorphMs — the sheet's own morph window", () => {
|
|
682
|
+
it("caps the container's full choreography at the family morph beat", () => {
|
|
683
|
+
// A height-changing swap reports enterDelay + morph (600ms); the sheet
|
|
684
|
+
// must resize inside the family's 300ms window, not twice as long.
|
|
685
|
+
expect(sheetMorphMs(600)).toBe(SHEET_MORPH_MS);
|
|
686
|
+
expect(sheetMorphMs(SHEET_MORPH_MS)).toBe(SHEET_MORPH_MS);
|
|
687
|
+
expect(sheetMorphMs(120)).toBe(120);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("maps a non-positive choreography to the instant snap", () => {
|
|
691
|
+
expect(sheetMorphMs(0)).toBe(0);
|
|
692
|
+
expect(sheetMorphMs(-1)).toBe(0);
|
|
693
|
+
expect(sheetMorphMs(Number.NaN)).toBe(0);
|
|
694
|
+
});
|
|
695
|
+
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
defineComponent,
|
|
3
3
|
h,
|
|
4
4
|
nextTick,
|
|
5
|
+
onBeforeUnmount,
|
|
5
6
|
ref,
|
|
6
7
|
watch,
|
|
7
8
|
type Component,
|
|
@@ -9,12 +10,24 @@ import {
|
|
|
9
10
|
} from "vue";
|
|
10
11
|
|
|
11
12
|
import HkTransitionContainer from "./HkTransitionContainer";
|
|
13
|
+
import { STEPFLOW_SWAP_EVENT } from "./HkStepFlow";
|
|
12
14
|
import HkScrollContainer from "./HkScrollContainer";
|
|
13
15
|
import HTabs from "./HkTabs";
|
|
14
16
|
import { useBreakpoint } from "../runtime/useBreakpoint";
|
|
15
17
|
import HModal from "./HkModal";
|
|
16
18
|
import "./HkSettingsDialog.scss";
|
|
17
19
|
|
|
20
|
+
/** The family's sheet-morph window (HkStepFlow announces the same beat):
|
|
21
|
+
* the pane container reports its WHOLE choreography (enter delay + morph),
|
|
22
|
+
* but the sheet's own resize must not inherit that doubling. */
|
|
23
|
+
export const SHEET_MORPH_MS = 300;
|
|
24
|
+
|
|
25
|
+
/** Morph duration to hand the modal for a settled swap. */
|
|
26
|
+
export function sheetMorphMs(choreographyMs: number): number {
|
|
27
|
+
if (!(choreographyMs > 0)) return 0;
|
|
28
|
+
return Math.min(choreographyMs, SHEET_MORPH_MS);
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
/** One rail entry of the settings dialog. */
|
|
19
32
|
export interface HkSettingsSection {
|
|
20
33
|
/** Identity of the section — also the name of the slot that renders its
|
|
@@ -63,6 +76,22 @@ export interface HkSettingsSection {
|
|
|
63
76
|
* Desktop keeps the rail exactly as it was; the flip is live on resize
|
|
64
77
|
* and shared by every consumer of this shell (page-embedded bodies
|
|
65
78
|
* included).
|
|
79
|
+
*
|
|
80
|
+
* Docked-sheet height hand-off (2026-11-04 chest report): inside a phone
|
|
81
|
+
* sheet the modal sizes the window from this shell's content, but it
|
|
82
|
+
* measures the frame through `.hk-modal-body-inner` — which the host
|
|
83
|
+
* rule pins at `height: 100%` (unscoped, so it pins the phone sheet too),
|
|
84
|
+
* so the observed box never changes on a section swap and the sheet froze
|
|
85
|
+
* at whichever section it OPENED with (the 470px About card inside a 24px
|
|
86
|
+
* pane; the freeze survives hikari #1041's phone fill). Every settled
|
|
87
|
+
* swap of the pane's morph container is therefore forwarded as the
|
|
88
|
+
* family's sheet protocol event (`hk-stepflow-swap`, detail
|
|
89
|
+
* `{phase: "morph", durationMs}`, where the duration is capped to the
|
|
90
|
+
* family's own sheet morph beat — the container reports its whole
|
|
91
|
+
* choreography, enter delay included — or `{phase: "instant"}` when the
|
|
92
|
+
* swap settled with no animation), which HkModal already turns into a
|
|
93
|
+
* frame re-measure. Page-embedded bodies (no modal ancestor) simply
|
|
94
|
+
* bubble it nowhere.
|
|
66
95
|
*/
|
|
67
96
|
export const HkSettingsBody = defineComponent({
|
|
68
97
|
name: "HkSettingsBody",
|
|
@@ -114,16 +143,109 @@ export const HkSettingsBody = defineComponent({
|
|
|
114
143
|
// typed here — the scroll reset needs the live viewport element and
|
|
115
144
|
// nothing else.
|
|
116
145
|
const paneRef = ref<{ getScrollElement(): HTMLElement | undefined } | null>(null);
|
|
146
|
+
const shellRef = ref<HTMLElement | null>(null);
|
|
147
|
+
const morphRef = ref<{ $el?: HTMLElement } | null>(null);
|
|
148
|
+
let stageBeforeSwap = 0;
|
|
149
|
+
// One instant-wait loop at a time: a new section change (or unmount)
|
|
150
|
+
// retires the previous epoch so a stale poll can never announce against
|
|
151
|
+
// the next swap's half-settled geometry.
|
|
152
|
+
let instantEpoch = 0;
|
|
153
|
+
let instantPoll: number | null = null;
|
|
154
|
+
const scheduleFrame = (cb: () => void): number =>
|
|
155
|
+
typeof requestAnimationFrame === "function"
|
|
156
|
+
? requestAnimationFrame(cb)
|
|
157
|
+
: window.setTimeout(cb, 16);
|
|
158
|
+
const cancelFrame = (id: number): void => {
|
|
159
|
+
if (typeof cancelAnimationFrame === "function") cancelAnimationFrame(id);
|
|
160
|
+
else clearTimeout(id);
|
|
161
|
+
};
|
|
162
|
+
const cancelInstantPoll = (): void => {
|
|
163
|
+
if (instantPoll !== null) {
|
|
164
|
+
cancelFrame(instantPoll);
|
|
165
|
+
instantPoll = null;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
onBeforeUnmount(() => {
|
|
169
|
+
instantEpoch += 1;
|
|
170
|
+
cancelInstantPoll();
|
|
171
|
+
});
|
|
117
172
|
// Section switches restart the pane from the top. Written through the
|
|
118
173
|
// public ref slice (the viewport element) rather than scrollTo so the
|
|
119
174
|
// reset stays synchronous and testable in no-layout environments.
|
|
120
175
|
watch(active, () => {
|
|
176
|
+
// The instant settle below needs the height the pane had BEFORE the
|
|
177
|
+
// swap; watchers run before the component patches, so this is still
|
|
178
|
+
// the outgoing stage.
|
|
179
|
+
instantEpoch += 1;
|
|
180
|
+
cancelInstantPoll();
|
|
181
|
+
stageBeforeSwap = morphRef.value?.$el?.offsetHeight ?? 0;
|
|
121
182
|
void nextTick(() => {
|
|
122
183
|
const viewport = paneRef.value?.getScrollElement();
|
|
123
184
|
if (viewport) viewport.scrollTop = 0;
|
|
124
185
|
});
|
|
125
186
|
});
|
|
126
187
|
|
|
188
|
+
// ── docked-sheet height hand-off ──────────────────────────────────
|
|
189
|
+
// A phone sheet is sized from this shell's content, but HkModal
|
|
190
|
+
// observes `.hk-modal-body-inner`, which the host rule pins at
|
|
191
|
+
// `height: 100%` (see the SCSS): the box never changes on a section
|
|
192
|
+
// swap, so the sheet would keep the section it OPENED with — about
|
|
193
|
+
// 24px of pane for a 470px 「About」 card (2026-11-04 chest report;
|
|
194
|
+
// the same freeze survives hikari #1041). The pane's own morph
|
|
195
|
+
// container already knows when a swap settles; forward that beat
|
|
196
|
+
// through the family's sheet protocol (`hk-stepflow-swap`, the one
|
|
197
|
+
// HkModal listens for) so the modal re-measures the frame.
|
|
198
|
+
//
|
|
199
|
+
// `durationMs` 0 = the swap settled instantly (reduced motion / the
|
|
200
|
+
// global animation switch) — the protocol's `instant` phase snaps the
|
|
201
|
+
// sheet instead of animating. That snap must not be announced from the
|
|
202
|
+
// settle beat itself: with no choreography the settle lands while the
|
|
203
|
+
// pane's crossfade still holds the LEAVING generation in the flow, so
|
|
204
|
+
// the modal would measure the outgoing (taller) height and a shrink
|
|
205
|
+
// would no-op — the sheet stayed 621px over a 251px section in the
|
|
206
|
+
// round-1 probe. Wait (bounded) for the stage to actually change, then
|
|
207
|
+
// hand the beat over; a growing swap has already changed by then and
|
|
208
|
+
// dispatches on the first frame.
|
|
209
|
+
function dispatchSheetSwap(durationMs: number): void {
|
|
210
|
+
const el = shellRef.value;
|
|
211
|
+
if (!el) return;
|
|
212
|
+
el.dispatchEvent(
|
|
213
|
+
new CustomEvent(STEPFLOW_SWAP_EVENT, {
|
|
214
|
+
bubbles: true,
|
|
215
|
+
detail:
|
|
216
|
+
durationMs > 0
|
|
217
|
+
? { phase: "morph", durationMs }
|
|
218
|
+
: { phase: "instant" },
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function announceSheetSwap(payload: { durationMs: number }): void {
|
|
224
|
+
if (!shellRef.value) return;
|
|
225
|
+
if (payload.durationMs > 0) {
|
|
226
|
+
dispatchSheetSwap(sheetMorphMs(payload.durationMs));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
cancelInstantPoll();
|
|
230
|
+
const epoch = instantEpoch;
|
|
231
|
+
const stage = (): number => morphRef.value?.$el?.offsetHeight ?? 0;
|
|
232
|
+
const before = stageBeforeSwap;
|
|
233
|
+
const deadline = Date.now() + 400;
|
|
234
|
+
const tick = (): void => {
|
|
235
|
+
if (epoch !== instantEpoch) {
|
|
236
|
+
instantPoll = null;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (stage() !== before || Date.now() >= deadline) {
|
|
240
|
+
instantPoll = null;
|
|
241
|
+
dispatchSheetSwap(0);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
instantPoll = scheduleFrame(tick);
|
|
245
|
+
};
|
|
246
|
+
instantPoll = scheduleFrame(tick);
|
|
247
|
+
}
|
|
248
|
+
|
|
127
249
|
function pick(key: string) {
|
|
128
250
|
// Belt-and-braces: every caller already refuses disabled entries
|
|
129
251
|
// upstream (rail: natively disabled buttons; strip: HTabs' disabled
|
|
@@ -144,7 +266,7 @@ export const HkSettingsBody = defineComponent({
|
|
|
144
266
|
const { isMobile } = useBreakpoint();
|
|
145
267
|
|
|
146
268
|
return () => (
|
|
147
|
-
<div class="hk-settings" data-phone={isMobile.value || undefined}>
|
|
269
|
+
<div class="hk-settings" data-phone={isMobile.value || undefined} ref={shellRef}>
|
|
148
270
|
{/* Scroll ownership (see the docblock): both scrollers are
|
|
149
271
|
HkScrollContainers — the overlay chrome replaces the native
|
|
150
272
|
bars, and the paired-class rules in the SCSS carry each
|
|
@@ -219,7 +341,12 @@ export const HkSettingsBody = defineComponent({
|
|
|
219
341
|
incoming section's height and the contents dissolve, the
|
|
220
342
|
same mechanism HkTabs panels use. A settings pane is a page
|
|
221
343
|
slot — it must not hard-cut. */}
|
|
222
|
-
<HkTransitionContainer
|
|
344
|
+
<HkTransitionContainer
|
|
345
|
+
ref={morphRef}
|
|
346
|
+
swapKey={active()}
|
|
347
|
+
axis="height"
|
|
348
|
+
onSwap={(payload: { durationMs: number }) => announceSheetSwap(payload)}
|
|
349
|
+
>
|
|
223
350
|
{slots[active()]?.()}
|
|
224
351
|
</HkTransitionContainer>
|
|
225
352
|
</HkScrollContainer>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
/* HkSolarLocationSetting — the solar-clock location strategy
|
|
1
|
+
/* HkSolarLocationSetting — the solar-clock location strategy card and
|
|
2
2
|
* its editor modal body (see the component docblock for the strategy
|
|
3
3
|
* ladder). AI-generated: developed with the ZCode agent (model
|
|
4
4
|
* GLM-5.3); human-directed. */
|
|
5
5
|
|
|
6
|
-
/* ── The settings-page
|
|
6
|
+
/* ── The settings-page card (HkCard root; header/body classes are the
|
|
7
|
+
* card family's own) ─────────────────────────────────────────────── */
|
|
7
8
|
.hk-solar-setting {
|
|
9
|
+
min-width: 0;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* ── The card body's live summary + Edit button line ──────────────────── */
|
|
13
|
+
.hk-solar-row {
|
|
8
14
|
display: flex;
|
|
9
15
|
align-items: center;
|
|
10
16
|
gap: var(--space-8, 0.5rem);
|
|
@@ -59,6 +65,61 @@
|
|
|
59
65
|
line-height: 1.5;
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
/* ── Detected-value readout (timezone / ip panes) ───────────────────────
|
|
69
|
+
* One info item per line: a fixed label column and a value column of
|
|
70
|
+
* copyable tags, so long values keep their own row and the two columns
|
|
71
|
+
* stay aligned across rows. */
|
|
72
|
+
.hk-solar-detected {
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
gap: var(--space-8, 0.5rem);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.hk-solar-detected-row {
|
|
79
|
+
display: grid;
|
|
80
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
81
|
+
gap: var(--space-8, 0.5rem);
|
|
82
|
+
align-items: start;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.hk-solar-detected-label {
|
|
86
|
+
padding-top: var(--space-1, 0.125rem);
|
|
87
|
+
font-size: var(--text-xs);
|
|
88
|
+
color: rgb(var(--color-muted));
|
|
89
|
+
/* Long localized labels wrap instead of pushing the value column out. */
|
|
90
|
+
overflow-wrap: anywhere;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.hk-solar-detected-value {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-wrap: wrap;
|
|
96
|
+
gap: var(--space-6, 0.375rem);
|
|
97
|
+
min-width: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* Values are labels, not prose — but a readout value that outruns the
|
|
101
|
+
* modal soft-wraps inside its chip instead of clipping or overflowing. */
|
|
102
|
+
.hk-solar-detected-value .hk-badge {
|
|
103
|
+
white-space: normal;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
text-align: left;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Probe status line (IP pane): muted progress / failure note, with the
|
|
109
|
+
* retry affordance inline after the text. */
|
|
110
|
+
.hk-solar-detected-status {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
gap: var(--space-6, 0.375rem);
|
|
115
|
+
color: rgb(var(--color-muted));
|
|
116
|
+
font-size: var(--text-xs);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hk-solar-detected-retry {
|
|
120
|
+
flex-shrink: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
62
123
|
.hk-solar-manual {
|
|
63
124
|
display: flex;
|
|
64
125
|
flex-direction: column;
|