adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
439 lines
9.6 KiB
Svelte
439 lines
9.6 KiB
Svelte
<script lang="ts">
|
||
import {
|
||
format_date,
|
||
generate_calendar_days,
|
||
calculate_display_hour,
|
||
convert_display_hour_to_24h,
|
||
month_names
|
||
} from "./utils";
|
||
|
||
let {
|
||
selected_date = $bindable<Date>(),
|
||
current_year = $bindable<number>(),
|
||
current_month = $bindable<number>(),
|
||
selected_hour = $bindable<number>(),
|
||
selected_minute = $bindable<number>(),
|
||
selected_second = $bindable<number>(),
|
||
is_pm = $bindable<boolean>(),
|
||
include_time,
|
||
position,
|
||
onclose,
|
||
onclear,
|
||
onupdate
|
||
}: {
|
||
selected_date: Date;
|
||
current_year: number;
|
||
current_month: number;
|
||
selected_hour: number;
|
||
selected_minute: number;
|
||
selected_second: number;
|
||
is_pm: boolean;
|
||
include_time: boolean;
|
||
position: { top: number; left: number };
|
||
onclose?: () => void;
|
||
onclear?: () => void;
|
||
onupdate?: (formatted: string) => void;
|
||
} = $props();
|
||
|
||
let display_hour = $derived(calculate_display_hour(selected_hour, is_pm));
|
||
let calendar_days = $derived(
|
||
generate_calendar_days(current_year, current_month)
|
||
);
|
||
|
||
const select_date = (day: number): void => {
|
||
selected_date = new Date(
|
||
current_year,
|
||
current_month,
|
||
day,
|
||
selected_hour,
|
||
selected_minute,
|
||
selected_second
|
||
);
|
||
update_value();
|
||
};
|
||
|
||
const update_value = (): void => {
|
||
const formatted = format_date(selected_date, include_time);
|
||
onupdate?.(formatted);
|
||
};
|
||
|
||
const update_time = (): void => {
|
||
selected_date = new Date(
|
||
current_year,
|
||
current_month,
|
||
selected_date.getDate(),
|
||
selected_hour,
|
||
selected_minute,
|
||
selected_second
|
||
);
|
||
update_value();
|
||
};
|
||
|
||
const previous_month = (): void => {
|
||
if (current_month === 0) {
|
||
current_month = 11;
|
||
current_year--;
|
||
} else {
|
||
current_month--;
|
||
}
|
||
};
|
||
|
||
const next_month = (): void => {
|
||
if (current_month === 11) {
|
||
current_month = 0;
|
||
current_year++;
|
||
} else {
|
||
current_month++;
|
||
}
|
||
};
|
||
|
||
const toggle_am_pm = (): void => {
|
||
is_pm = !is_pm;
|
||
if (is_pm && selected_hour < 12) {
|
||
selected_hour += 12;
|
||
} else if (!is_pm && selected_hour >= 12) {
|
||
selected_hour -= 12;
|
||
}
|
||
update_time();
|
||
};
|
||
|
||
const update_display_hour = (new_hour: number): void => {
|
||
selected_hour = convert_display_hour_to_24h(new_hour, is_pm);
|
||
update_time();
|
||
};
|
||
|
||
const handle_now = (): void => {
|
||
const now = new Date();
|
||
selected_date = now;
|
||
current_year = now.getFullYear();
|
||
current_month = now.getMonth();
|
||
selected_hour = now.getHours();
|
||
selected_minute = now.getMinutes();
|
||
selected_second = now.getSeconds();
|
||
is_pm = selected_hour >= 12;
|
||
update_value();
|
||
};
|
||
</script>
|
||
|
||
<div
|
||
class="picker-container"
|
||
style="top: {position.top}px; left: {position.left}px;"
|
||
>
|
||
<div class="picker">
|
||
<div class="picker-header">
|
||
<button type="button" class="nav-button" onclick={previous_month}
|
||
>‹</button
|
||
>
|
||
<div class="month-year">
|
||
{month_names[current_month]}
|
||
{current_year}
|
||
</div>
|
||
<button type="button" class="nav-button" onclick={next_month}>›</button>
|
||
</div>
|
||
|
||
<div class="calendar-grid">
|
||
<div class="weekdays">
|
||
<div class="weekday">Su</div>
|
||
<div class="weekday">Mo</div>
|
||
<div class="weekday">Tu</div>
|
||
<div class="weekday">We</div>
|
||
<div class="weekday">Th</div>
|
||
<div class="weekday">Fr</div>
|
||
<div class="weekday">Sa</div>
|
||
</div>
|
||
|
||
<div class="days">
|
||
{#each calendar_days as { day, is_current_month, is_next_month }}
|
||
<button
|
||
type="button"
|
||
class="day"
|
||
class:other-month={!is_current_month}
|
||
class:selected={is_current_month &&
|
||
day === selected_date.getDate() &&
|
||
current_month === selected_date.getMonth() &&
|
||
current_year === selected_date.getFullYear()}
|
||
onclick={() => {
|
||
if (is_current_month) {
|
||
select_date(day);
|
||
} else if (is_next_month) {
|
||
next_month();
|
||
select_date(day);
|
||
} else {
|
||
previous_month();
|
||
select_date(day);
|
||
}
|
||
}}
|
||
>
|
||
{day}
|
||
</button>
|
||
{/each}
|
||
</div>
|
||
</div>
|
||
|
||
{#if include_time}
|
||
<div class="time-picker">
|
||
<div class="time-inputs">
|
||
<div class="time-input-group">
|
||
<label for="hour">Hour</label>
|
||
<input
|
||
id="hour"
|
||
type="number"
|
||
min="1"
|
||
max="12"
|
||
bind:value={display_hour}
|
||
oninput={() => update_display_hour(display_hour)}
|
||
/>
|
||
</div>
|
||
<div class="time-input-group">
|
||
<label for="minute">Min</label>
|
||
<input
|
||
id="minute"
|
||
type="number"
|
||
min="0"
|
||
max="59"
|
||
bind:value={selected_minute}
|
||
oninput={update_time}
|
||
/>
|
||
</div>
|
||
<div class="time-input-group">
|
||
<label for="second">Sec</label>
|
||
<input
|
||
id="second"
|
||
type="number"
|
||
min="0"
|
||
max="59"
|
||
bind:value={selected_second}
|
||
oninput={update_time}
|
||
/>
|
||
</div>
|
||
<div class="time-input-group">
|
||
<span class="am-pm-label">Period</span>
|
||
<button
|
||
type="button"
|
||
class="am-pm-toggle"
|
||
onclick={toggle_am_pm}
|
||
aria-label="Toggle AM/PM"
|
||
>
|
||
{is_pm ? "PM" : "AM"}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
|
||
<div class="picker-actions">
|
||
<button type="button" class="action-button" onclick={onclear}>
|
||
Clear
|
||
</button>
|
||
<div class="picker-actions-right">
|
||
<button type="button" class="action-button" onclick={handle_now}>
|
||
Now
|
||
</button>
|
||
<button type="button" class="action-button" onclick={onclose}>
|
||
Done
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.picker-container {
|
||
position: fixed;
|
||
z-index: 9999;
|
||
box-shadow: var(--shadow-drop-lg);
|
||
border-radius: var(--radius-lg);
|
||
background: var(--background-fill-primary);
|
||
border: 1px solid var(--border-color-primary);
|
||
}
|
||
|
||
.picker {
|
||
padding: var(--size-3);
|
||
min-width: 280px;
|
||
}
|
||
|
||
.picker-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: var(--size-3);
|
||
}
|
||
|
||
.nav-button {
|
||
background: none;
|
||
border: none;
|
||
font-size: var(--text-lg);
|
||
padding: var(--size-1);
|
||
border-radius: var(--radius-sm);
|
||
transition: var(--button-transition);
|
||
cursor: pointer;
|
||
color: var(--body-text-color-subdued);
|
||
}
|
||
|
||
.nav-button:hover {
|
||
background: var(--button-secondary-background-fill-hover);
|
||
color: var(--body-text-color);
|
||
}
|
||
|
||
.month-year {
|
||
font-weight: var(--weight-semibold);
|
||
font-size: var(--text-base);
|
||
color: var(--body-text-color);
|
||
}
|
||
|
||
.calendar-grid {
|
||
margin-bottom: var(--size-3);
|
||
}
|
||
|
||
.weekdays {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 1px;
|
||
margin-bottom: var(--size-2);
|
||
}
|
||
|
||
.weekday {
|
||
text-align: center;
|
||
font-size: var(--text-sm);
|
||
font-weight: var(--weight-semibold);
|
||
color: var(--body-text-color-subdued);
|
||
padding: var(--size-1);
|
||
}
|
||
|
||
.days {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, 1fr);
|
||
gap: 1px;
|
||
}
|
||
|
||
.day {
|
||
aspect-ratio: 1;
|
||
border: none;
|
||
background: none;
|
||
border-radius: var(--radius-sm);
|
||
font-size: var(--text-sm);
|
||
transition: var(--button-transition);
|
||
color: var(--body-text-color);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.day:hover {
|
||
background: var(--button-secondary-background-fill-hover);
|
||
}
|
||
|
||
.day.other-month {
|
||
color: var(--body-text-color-subdued);
|
||
}
|
||
|
||
.day.selected {
|
||
background: var(--button-primary-background-fill);
|
||
color: var(--button-primary-text-color);
|
||
}
|
||
|
||
.day.selected:hover {
|
||
background: var(--button-primary-background-fill-hover);
|
||
}
|
||
|
||
.time-picker {
|
||
border-top: 1px solid var(--border-color-primary);
|
||
padding-top: var(--size-3);
|
||
margin-bottom: var(--size-3);
|
||
}
|
||
|
||
.time-inputs {
|
||
display: flex;
|
||
gap: var(--size-2);
|
||
justify-content: center;
|
||
}
|
||
|
||
.time-input-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: var(--size-1);
|
||
}
|
||
|
||
.time-input-group label {
|
||
font-size: var(--text-xs);
|
||
color: var(--body-text-color-subdued);
|
||
font-weight: var(--weight-semibold);
|
||
}
|
||
|
||
.am-pm-label {
|
||
font-size: var(--text-xs);
|
||
color: var(--body-text-color-subdued);
|
||
font-weight: var(--weight-semibold);
|
||
}
|
||
|
||
.time-input-group input {
|
||
width: 50px;
|
||
padding: var(--size-1);
|
||
border: 1px solid var(--input-border-color);
|
||
border-radius: var(--radius-sm);
|
||
text-align: center;
|
||
font-size: var(--text-sm);
|
||
background: var(--input-background-fill);
|
||
color: var(--body-text-color);
|
||
}
|
||
|
||
.time-input-group input:focus {
|
||
outline: none;
|
||
border-color: var(--input-border-color-focus);
|
||
box-shadow: var(--input-shadow-focus);
|
||
}
|
||
|
||
.am-pm-toggle {
|
||
width: 50px;
|
||
padding: var(--size-1);
|
||
border: 1px solid var(--button-primary-border-color);
|
||
border-radius: var(--radius-sm);
|
||
text-align: center;
|
||
font-size: var(--text-sm);
|
||
background: var(--button-primary-background-fill);
|
||
color: var(--button-primary-text-color);
|
||
cursor: pointer;
|
||
transition: var(--button-transition);
|
||
}
|
||
|
||
.am-pm-toggle:hover {
|
||
background: var(--button-primary-background-fill-hover);
|
||
border-color: var(--button-primary-border-color-hover);
|
||
}
|
||
|
||
.am-pm-toggle:focus {
|
||
outline: none;
|
||
border-color: var(--button-primary-border-color-focus);
|
||
box-shadow: var(--button-primary-shadow-focus);
|
||
}
|
||
|
||
.picker-actions {
|
||
display: flex;
|
||
gap: var(--size-2);
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
border-top: 1px solid var(--border-color-primary);
|
||
padding-top: var(--size-3);
|
||
}
|
||
|
||
.picker-actions-right {
|
||
display: flex;
|
||
gap: var(--size-2);
|
||
}
|
||
|
||
.action-button {
|
||
padding: var(--size-1) var(--size-3);
|
||
border: 1px solid var(--button-secondary-border-color);
|
||
border-radius: var(--radius-sm);
|
||
background: var(--button-secondary-background-fill);
|
||
color: var(--button-secondary-text-color);
|
||
font-size: var(--text-sm);
|
||
transition: var(--button-transition);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.action-button:hover {
|
||
background: var(--button-secondary-background-fill-hover);
|
||
border-color: var(--button-secondary-border-color-hover);
|
||
}
|
||
</style>
|