diff --git a/apps/docs/docs/widgets/air-quality/index.mdx b/apps/docs/docs/widgets/air-quality/index.mdx index c1b7dd4bd..1e37bbd3b 100644 --- a/apps/docs/docs/widgets/air-quality/index.mdx +++ b/apps/docs/docs/widgets/air-quality/index.mdx @@ -18,9 +18,9 @@ dominant pollutant, particulate matter and the highest available pollen reading view for a 24-hour AQI and UV chart, all available pollutant and pollen readings, and seven-day daily maxima. Select the European or US AQI scale, or use the automatic setting. Automatic uses the US scale for viewers whose -locale region is the United States and the European scale everywhere else. The selected scale is always shown next -to the value. Pollen values are individual measured concentrations; the widget does not combine them into a single -pollen index. +locale region is the United States and the European scale everywhere else. The selected scale stays in widget options +instead of being repeated beside every reading. Pollen values are individual measured concentrations; the widget does +not combine them into a single pollen index. The categories and colors summarize published index bands and are not medical advice. Pollutant concentrations use micrograms per cubic meter (`µg/m³`), and pollen uses grains per cubic meter. diff --git a/apps/docs/docs/widgets/clock/index.mdx b/apps/docs/docs/widgets/clock/index.mdx index 1f453d33f..2ee0edf57 100644 --- a/apps/docs/docs/widgets/clock/index.mdx +++ b/apps/docs/docs/widgets/clock/index.mdx @@ -24,10 +24,9 @@ comparable card. The primary clock includes a segmented 24-hour rail with a curr time, six-hour ticks, and a legend for night, dawn, day, and dusk. These phases are based on local clock time and are not astronomical sunrise or sunset calculations. Secondary cards stop after their localized date and timezone offsets. -The compact widget remains a single numeric clock. The time-format selector shows common Day.js presets with live -examples and applies to the primary clock. Choosing a format with seconds enables second-level updates; 12-hour and -24-hour display are selected through those same formats. The date-format selector likewise -shows live Day.js examples and their tokens. Secondary cities always include a short localized date and a +The compact widget remains a single numeric clock. The 12/24-hour and seconds switches apply unless a custom Day.js +time format overrides them. The date-format selector shows common presets with live examples, while the custom date +format preserves support for any compatible Day.js tokens. Secondary cities always include a short localized date and a Yesterday, Today, or Tomorrow label relative to the primary timezone. UTC and relative offsets follow current timezone rules, including daylight-saving changes. diff --git a/apps/docs/docs/widgets/clock/index.ts b/apps/docs/docs/widgets/clock/index.ts index f34cae4ed..5a516b404 100644 --- a/apps/docs/docs/widgets/clock/index.ts +++ b/apps/docs/docs/widgets/clock/index.ts @@ -20,6 +20,18 @@ export const clockWidget: WidgetDefinition = { values: { type: "string" }, defaultValue: "-", }, + { + name: "24-hour format", + description: "Use 24-hour format instead of 12-hour format when no custom time format is set.", + values: { type: "boolean" }, + defaultValue: "yes", + }, + { + name: "Display seconds", + description: "Show seconds when no custom time format is set.", + values: { type: "boolean" }, + defaultValue: "no", + }, { name: "Use fixed timezone", description: "Select custom timezone to display instead of the client timezone.", @@ -45,11 +57,16 @@ export const clockWidget: WidgetDefinition = { defaultValue: "dddd, MMMM D", }, { - name: "Time format", - description: - "Choose a common Day.js time format with a live example. The format controls 12/24-hour display and seconds.", - values: "HH:mm, HH:mm:ss, h:mm A, or h:mm:ss A", - defaultValue: "HH:mm", + name: "Custom time format", + description: "Use Day.js tokens to override the 12/24-hour and seconds options.", + values: "A Day.js format such as HH:mm:ss or h:mm A", + defaultValue: "-", + }, + { + name: "Custom date format", + description: "Use Day.js tokens to override the selected date format.", + values: "A Day.js format such as YYYY-MM-DD", + defaultValue: "-", }, { name: "Show weather", diff --git a/apps/docs/docs/widgets/timer/index.mdx b/apps/docs/docs/widgets/timer/index.mdx index 9b3a416cf..a0bf2f670 100644 --- a/apps/docs/docs/widgets/timer/index.mdx +++ b/apps/docs/docs/widgets/timer/index.mdx @@ -35,8 +35,9 @@ supplement the visible timer state rather than replacing it. When automatic start is disabled, a completed phase labels the next step as ready and flashes the widget edges with the board's secondary color. The border glow and completion chime run together as a five-beat alarm burst at 300-millisecond intervals, then stop. Reduced-motion and forced-colors modes replace the flash with a static border. -Compact labels, timer text, and controls use the dashboard theme's responsive size scale, with secondary details hidden -when the widget preview is narrow or short. +The current phase stays pinned to the compact widget's top-left corner and wraps when needed. Timer text and controls +use the dashboard theme's responsive size scale, with secondary details hidden when the widget preview is narrow or +short. Alerts can fire while Homarr is open. Browser and operating-system notification settings may still suppress them. The timer's mode, durations, session count, and automatic-start behavior are permanent board settings and are edited diff --git a/apps/docs/static/api/open-api-schema.json b/apps/docs/static/api/open-api-schema.json index 0b1bdd822..8e5da38c1 100644 --- a/apps/docs/static/api/open-api-schema.json +++ b/apps/docs/static/api/open-api-schema.json @@ -2106,6 +2106,10 @@ "enum": [ "clock", "weather", + "today", + "airQuality", + "countdown", + "timer", "app", "iframe", "video", diff --git a/packages/request-handler/src/weather.spec.ts b/packages/request-handler/src/weather.spec.ts index a5e8e767c..b89acfc81 100644 --- a/packages/request-handler/src/weather.spec.ts +++ b/packages/request-handler/src/weather.spec.ts @@ -75,6 +75,7 @@ describe("weather request handler", () => { it("requests bounded useful forecasts and normalizes location times", async () => { const response = makeWeatherResponse(); response.hourly.temperature_2m = [null]; + Reflect.deleteProperty(response.hourly, "wind_gusts_10m"); response.daily.sunrise[1] = null; mockFetch.mockResolvedValue(new Response(JSON.stringify(response))); @@ -87,6 +88,7 @@ describe("weather request handler", () => { expect(first.data.daily[0]?.date).toBe("2026-07-01"); expect(first.data.hourly[0]?.temperature).toBeNull(); expect(first.data.hourly[1]?.temperature).toBeNull(); + expect(first.data.hourly[0]?.windGusts).toBeNull(); expect(first.data.daily[0]?.sunriseAt).toBeNull(); expect(first.data.daily[1]?.sunriseAt).toBe("2026-07-02T03:00:00.000Z"); expect(second.data).toStrictEqual(first.data); diff --git a/packages/request-handler/src/weather.ts b/packages/request-handler/src/weather.ts index 749880b22..24d29ed1d 100644 --- a/packages/request-handler/src/weather.ts +++ b/packages/request-handler/src/weather.ts @@ -144,7 +144,8 @@ export const weatherRequestHandler = createWidgetRequestHandler({ requestAsync: requestWeatherAsync, }); -const nullableNumberArray = z.array(z.number().nullable()); +const nullableNumberArray = z.array(z.number().nullable()).default([]); +const nullableStringArray = z.array(z.string().nullable()).default([]); const atLocationOutput = z.object({ timezone: z.string().min(1), @@ -184,8 +185,8 @@ const atLocationOutput = z.object({ wind_speed_10m_max: nullableNumberArray, wind_gusts_10m_max: nullableNumberArray, uv_index_max: nullableNumberArray, - sunrise: z.array(z.string().nullable()), - sunset: z.array(z.string().nullable()), + sunrise: nullableStringArray, + sunset: nullableStringArray, daylight_duration: nullableNumberArray, }), }); diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index f68859071..5a00c70ed 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -3153,6 +3153,9 @@ "label": "24-hour format", "description": "Use 24-hour format instead of 12-hour format" }, + "showSeconds": { + "label": "Display seconds" + }, "useCustomTimezone": { "label": "Use a fixed timezone" }, @@ -3168,8 +3171,12 @@ "description": "Choose a Day.js preset. Each option shows a live example and its format tokens." }, "customTimeFormat": { - "label": "Time format", - "description": "Choose a Day.js preset. The selected format controls 12/24-hour display and whether the clock updates every second." + "label": "Custom time format", + "description": "Use Day.js tokens to override the 12/24-hour and seconds options." + }, + "customDateFormat": { + "label": "Custom date format", + "description": "Use Day.js tokens to override the selected date format." }, "showWeather": { "label": "Show weather", diff --git a/packages/widgets/src/clock/advanced-view.tsx b/packages/widgets/src/clock/advanced-view.tsx index 78ac239af..1fff55291 100644 --- a/packages/widgets/src/clock/advanced-view.tsx +++ b/packages/widgets/src/clock/advanced-view.tsx @@ -222,11 +222,13 @@ const ClockTime = ({ if (!entry.zonedTime) return null; let formattedTime = formatLocalizedTime(now, locale, { hour12: clockTimeFormatUses12Hours(options.customTimeFormat, options.is24HourFormat), - includeSeconds: clockTimeFormatShowsSeconds(options.customTimeFormat), + includeSeconds: clockTimeFormatShowsSeconds(options.customTimeFormat, options.showSeconds), timeZone: entry.timeZone, }); if (primary) { - formattedTime = entry.zonedTime.format(resolveClockTimeFormat(options.customTimeFormat, options.is24HourFormat)); + formattedTime = entry.zonedTime.format( + resolveClockTimeFormat(options.customTimeFormat, options.is24HourFormat, options.showSeconds), + ); } const accessibleDate = formatLocalizedDate(now, locale, { dateStyle: "medium", diff --git a/packages/widgets/src/clock/component.tsx b/packages/widgets/src/clock/component.tsx index 45180cff9..4eea4b8c2 100644 --- a/packages/widgets/src/clock/component.tsx +++ b/packages/widgets/src/clock/component.tsx @@ -15,7 +15,7 @@ dayjs.extend(advancedFormat); export default function ClockWidget({ options, width, height, displayMode }: WidgetComponentProps<"clock">) { const isAdvanced = displayMode === "advanced"; - const showSeconds = clockTimeFormatShowsSeconds(options.customTimeFormat); + const showSeconds = clockTimeFormatShowsSeconds(options.customTimeFormat, options.showSeconds); const time = useCurrentTime({ showSeconds }); const requestedTimeZone = options.useCustomTimezone ? options.timezone : getResolvedLocalTimeZone(); const primaryTimeZoneInvalid = !isTimeZoneSupported(requestedTimeZone); @@ -37,7 +37,12 @@ export default function ClockWidget({ options, width, height, displayMode }: Wid return