slotlock 0.7.0 → 0.9.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.
- package/CHANGELOG.md +281 -0
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +64 -9
- package/assistant.js +450 -82
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +21 -0
- package/dashboard-assistant-ui.js +539 -0
- package/dashboard-markdown.d.ts +2 -0
- package/dashboard-markdown.js +157 -0
- package/dashboard-settings-ui.d.ts +1 -0
- package/dashboard-settings-ui.js +78 -0
- package/dashboard.d.ts +84 -11
- package/dashboard.js +1654 -384
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- package/types.d.ts +198 -0
package/booking-rules.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SlotlockRankedException } from './hours.js';
|
|
1
2
|
import type { Interval, WeeklyAvailabilityRule } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* A resource's booking rules: what a booking made through Slotlock (hold, reservation, agent event)
|
|
@@ -24,10 +25,46 @@ export interface SlotlockBookingRules {
|
|
|
24
25
|
*/
|
|
25
26
|
bufferBeforeMinutes?: number;
|
|
26
27
|
/**
|
|
27
|
-
* Minutes kept free after each booking made under the rule, as its trailing buffer: 0-
|
|
28
|
-
* Bookings made before the rule, or imported,
|
|
28
|
+
* Minutes kept free after each booking made under the rule, as its trailing buffer: 0-10,080 (a
|
|
29
|
+
* week, for a vehicle's service after a long hire). Bookings made before the rule, or imported,
|
|
30
|
+
* keep what they have.
|
|
29
31
|
*/
|
|
30
32
|
turnaroundMinutes?: number;
|
|
33
|
+
/** A booking lasts at least this many minutes: 1-525,600 (a year). */
|
|
34
|
+
minDurationMinutes?: number;
|
|
35
|
+
/** A booking lasts at most this many minutes: 1-525,600. */
|
|
36
|
+
maxDurationMinutes?: number;
|
|
37
|
+
/** A booking lasts exactly one of these many minutes: 1-20 lengths, each 1-525,600. */
|
|
38
|
+
allowedDurations?: number[];
|
|
39
|
+
/** The length a search offers when it is asked for none: 1-525,600, and allowed by the others. */
|
|
40
|
+
defaultDurationMinutes?: number;
|
|
41
|
+
/**
|
|
42
|
+
* A booking from one local day to another (a hire) starts inside one of these windows of the day,
|
|
43
|
+
* in the resource's time zone: 1-4 windows. A booking within one day is not held to them.
|
|
44
|
+
*/
|
|
45
|
+
pickupTimes?: SlotlockDayWindow[];
|
|
46
|
+
/** Such a booking ends inside one of these windows of the day, alike. */
|
|
47
|
+
returnTimes?: SlotlockDayWindow[];
|
|
48
|
+
/**
|
|
49
|
+
* How the bookable hours hold a booking from one local day to another (a hire): `whole`, every
|
|
50
|
+
* minute of it inside them; `ends`, only its start and end, the resource being out between. A
|
|
51
|
+
* booking within one day lies inside them either way. By default `ends` where `pickupTimes` or
|
|
52
|
+
* `returnTimes` are given, else `whole`. Writes are held to it under `enforceHours`; searches
|
|
53
|
+
* (slots and ranges) follow it always.
|
|
54
|
+
*/
|
|
55
|
+
hoursApply?: SlotlockHoursApply;
|
|
56
|
+
/**
|
|
57
|
+
* A booking is cancelled at least this many minutes before it starts (RFC 0004 B5): 0-525,600.
|
|
58
|
+
* Later, cancelling it is refused (`cancel_cutoff`) unless an admin overrides with a reason.
|
|
59
|
+
*/
|
|
60
|
+
cancelCutoffMinutes?: number;
|
|
61
|
+
/** A booking is moved at least this many minutes before it starts, alike (`reschedule_cutoff`). */
|
|
62
|
+
rescheduleCutoffMinutes?: number;
|
|
63
|
+
/**
|
|
64
|
+
* What a cancellation costs, recorded on it for the integrator to charge: Slotlock never moves
|
|
65
|
+
* money. With `withinMinutes`, only a cancellation that late before the start.
|
|
66
|
+
*/
|
|
67
|
+
cancellationFee?: SlotlockCancellationFee;
|
|
31
68
|
/** A booking lies inside the resource's bookable hours. */
|
|
32
69
|
enforceHours?: boolean;
|
|
33
70
|
/**
|
|
@@ -43,8 +80,20 @@ export interface SlotlockBookingRules {
|
|
|
43
80
|
*/
|
|
44
81
|
requireCertain?: boolean;
|
|
45
82
|
}
|
|
83
|
+
/** A cancellation's fee: an amount in the currency's minor unit (cents), ISO 4217 currency. */
|
|
84
|
+
export interface SlotlockCancellationFee {
|
|
85
|
+
amountMinor: number;
|
|
86
|
+
currency: string;
|
|
87
|
+
/** Only a cancellation this many minutes or fewer before the start: 0-525,600. */
|
|
88
|
+
withinMinutes?: number;
|
|
89
|
+
}
|
|
90
|
+
/** A window of a day: minutes after local midnight, and how long (it may run past midnight). */
|
|
91
|
+
export interface SlotlockDayWindow {
|
|
92
|
+
startMinutes: number;
|
|
93
|
+
durationMinutes: number;
|
|
94
|
+
}
|
|
46
95
|
/** Which rule a write broke, as its refusal names it. */
|
|
47
|
-
export type SlotlockBookingRule = 'min_notice' | 'max_advance' | 'slot_step' | 'outside_hours' | 'uncertain';
|
|
96
|
+
export type SlotlockBookingRule = 'min_notice' | 'max_advance' | 'slot_step' | 'outside_hours' | 'uncertain' | 'min_duration' | 'max_duration' | 'duration_not_allowed' | 'pickup_time' | 'return_time' | 'cancel_cutoff' | 'reschedule_cutoff';
|
|
48
97
|
/** Each numeric rule's least and greatest value. */
|
|
49
98
|
export declare const SLOTLOCK_BOOKING_RULE_LIMITS: Readonly<{
|
|
50
99
|
readonly minNoticeMinutes: readonly [0, 525600];
|
|
@@ -54,7 +103,29 @@ export declare const SLOTLOCK_BOOKING_RULE_LIMITS: Readonly<{
|
|
|
54
103
|
readonly bufferBeforeMinutes: readonly [0, number];
|
|
55
104
|
readonly turnaroundMinutes: readonly [0, number];
|
|
56
105
|
readonly checkInMinutes: readonly [1, number];
|
|
106
|
+
readonly minDurationMinutes: readonly [1, 525600];
|
|
107
|
+
readonly maxDurationMinutes: readonly [1, 525600];
|
|
108
|
+
readonly defaultDurationMinutes: readonly [1, 525600];
|
|
109
|
+
readonly cancelCutoffMinutes: readonly [0, 525600];
|
|
110
|
+
readonly rescheduleCutoffMinutes: readonly [0, 525600];
|
|
57
111
|
}>;
|
|
112
|
+
/** Lengths `allowedDurations` may list. */
|
|
113
|
+
export declare const SLOTLOCK_MAX_ALLOWED_DURATIONS = 20;
|
|
114
|
+
/** Windows `pickupTimes` and `returnTimes` may each hold. */
|
|
115
|
+
export declare const SLOTLOCK_MAX_DAY_WINDOWS = 4;
|
|
116
|
+
/** How the hours hold a hire (`hoursApply`). */
|
|
117
|
+
export type SlotlockHoursApply = 'whole' | 'ends';
|
|
118
|
+
/** The `hoursApply` a resource's rules mean: as given, else `ends` with pick-up or return times. */
|
|
119
|
+
export declare function hoursApplyOf(rules: SlotlockBookingRules | undefined): SlotlockHoursApply;
|
|
120
|
+
/**
|
|
121
|
+
* The cutoff rule a cancellation or move of a booking starting at `start`, made at `now`, breaks,
|
|
122
|
+
* or null (RFC 0004 B5).
|
|
123
|
+
*/
|
|
124
|
+
export declare function cutoffRuleBroken(rules: SlotlockBookingRules | undefined, kind: 'cancel' | 'reschedule', start: Date, now: Date): 'cancel_cutoff' | 'reschedule_cutoff' | null;
|
|
125
|
+
/** The fee a cancellation of a booking starting at `start`, made at `now`, records, or null. */
|
|
126
|
+
export declare function cancellationFeeDue(rules: SlotlockBookingRules | undefined, start: Date, now: Date): SlotlockCancellationFee | null;
|
|
127
|
+
/** The duration rule a booking of `minutes` breaks, or null. */
|
|
128
|
+
export declare function durationRuleBroken(rules: SlotlockBookingRules | undefined, minutes: number): 'min_duration' | 'max_duration' | 'duration_not_allowed' | null;
|
|
58
129
|
/** Booking rules from untrusted input, or a TypeError naming the field that is wrong. */
|
|
59
130
|
export declare function parseBookingRules(value: unknown): SlotlockBookingRules;
|
|
60
131
|
/**
|
|
@@ -83,13 +154,27 @@ export declare function bookingBufferBeforeMs(rules: SlotlockBookingRules | unde
|
|
|
83
154
|
*/
|
|
84
155
|
export declare function nextStepStart(from: Date, stepMinutes: number, timezone: string, offsetMinutes?: number): Date;
|
|
85
156
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
157
|
+
* Whether a booking runs from one local day into another (a hire): its last moment falls on a later
|
|
158
|
+
* local day than its start. One that ends at midnight stays on its day.
|
|
159
|
+
*/
|
|
160
|
+
export declare function isMultiDay(interval: Interval, timezone: string): boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Whether `instant` lies in one of the windows of its local day: `[start, start + length)`, or with
|
|
163
|
+
* `closed` (a return) up to and including the window's end.
|
|
164
|
+
*/
|
|
165
|
+
export declare function withinDayWindows(instant: Date, windows: readonly SlotlockDayWindow[], timezone: string, closed?: boolean): boolean;
|
|
166
|
+
/**
|
|
167
|
+
* The rule a booking of `interval` made at `now` breaks, or null. `hours` (the resource's own, else
|
|
168
|
+
* inherited, else the default) and `exceptions` are read only when enforced. A hire (a booking from
|
|
169
|
+
* one local day to another) under `pickupTimes` or `returnTimes` starts and ends in them, and under
|
|
170
|
+
* `enforceHours` lies inside the hours as `hoursApply` says: wholly, or only at its start and end
|
|
171
|
+
* (the resource being out between).
|
|
88
172
|
*/
|
|
89
173
|
export declare function bookingRuleBroken(params: {
|
|
90
174
|
rules: SlotlockBookingRules | undefined;
|
|
91
175
|
timezone: string;
|
|
92
176
|
hours: readonly WeeklyAvailabilityRule[];
|
|
177
|
+
exceptions?: readonly SlotlockRankedException[];
|
|
93
178
|
interval: Interval;
|
|
94
179
|
now: Date;
|
|
95
180
|
}): SlotlockBookingRule | null;
|
|
@@ -114,6 +199,8 @@ export declare function bookingSearch(params: {
|
|
|
114
199
|
window: Interval;
|
|
115
200
|
now: Date;
|
|
116
201
|
bufferAfterMs?: number;
|
|
202
|
+
/** The length searched for: a length the duration rules refuse finds nothing. */
|
|
203
|
+
durationMs?: number;
|
|
117
204
|
}): SlotlockBookingSearch | null;
|
|
118
205
|
/** The time a search must read as free or busy: its window widened by a slot's buffers. */
|
|
119
206
|
export declare function bookingSearchReach(search: SlotlockBookingSearch): Interval;
|
package/booking-rules.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { coveredByOpenWindows, openWindows } from './hours.js';
|
|
2
2
|
import { zonedOffsetMinutes } from './timezone.js';
|
|
3
3
|
const MINUTE_MS = 60_000;
|
|
4
4
|
const DAY_MINUTES = 24 * 60;
|
|
@@ -9,10 +9,131 @@ export const SLOTLOCK_BOOKING_RULE_LIMITS = Object.freeze({
|
|
|
9
9
|
slotStepMinutes: [1, DAY_MINUTES],
|
|
10
10
|
stepOffsetMinutes: [0, DAY_MINUTES - 1],
|
|
11
11
|
bufferBeforeMinutes: [0, DAY_MINUTES],
|
|
12
|
-
turnaroundMinutes: [0, DAY_MINUTES],
|
|
12
|
+
turnaroundMinutes: [0, 7 * DAY_MINUTES],
|
|
13
13
|
checkInMinutes: [1, DAY_MINUTES],
|
|
14
|
+
minDurationMinutes: [1, 525_600],
|
|
15
|
+
maxDurationMinutes: [1, 525_600],
|
|
16
|
+
defaultDurationMinutes: [1, 525_600],
|
|
17
|
+
cancelCutoffMinutes: [0, 525_600],
|
|
18
|
+
rescheduleCutoffMinutes: [0, 525_600],
|
|
14
19
|
});
|
|
15
20
|
const LIMITS = SLOTLOCK_BOOKING_RULE_LIMITS;
|
|
21
|
+
/** Lengths `allowedDurations` may list. */
|
|
22
|
+
export const SLOTLOCK_MAX_ALLOWED_DURATIONS = 20;
|
|
23
|
+
/** Windows `pickupTimes` and `returnTimes` may each hold. */
|
|
24
|
+
export const SLOTLOCK_MAX_DAY_WINDOWS = 4;
|
|
25
|
+
const LIST_RULES = new Set(['allowedDurations', 'pickupTimes', 'returnTimes', 'cancellationFee']);
|
|
26
|
+
/** The `hoursApply` a resource's rules mean: as given, else `ends` with pick-up or return times. */
|
|
27
|
+
export function hoursApplyOf(rules) {
|
|
28
|
+
if (rules?.hoursApply !== undefined)
|
|
29
|
+
return rules.hoursApply;
|
|
30
|
+
return rules?.pickupTimes !== undefined || rules?.returnTimes !== undefined ? 'ends' : 'whole';
|
|
31
|
+
}
|
|
32
|
+
function parseHoursApply(value) {
|
|
33
|
+
if (value !== 'whole' && value !== 'ends')
|
|
34
|
+
throw new TypeError("Slotlock booking rule hoursApply must be 'whole' or 'ends'");
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
/** `cancellationFee` from untrusted input, or a TypeError. */
|
|
38
|
+
function parseCancellationFee(value) {
|
|
39
|
+
const fee = value;
|
|
40
|
+
if (typeof fee !== 'object' ||
|
|
41
|
+
fee === null ||
|
|
42
|
+
Array.isArray(fee) ||
|
|
43
|
+
Object.keys(fee).some((key) => !['amountMinor', 'currency', 'withinMinutes'].includes(key)) ||
|
|
44
|
+
!Number.isSafeInteger(fee.amountMinor) ||
|
|
45
|
+
fee.amountMinor < 0 ||
|
|
46
|
+
fee.amountMinor > 1_000_000_000_000 ||
|
|
47
|
+
typeof fee.currency !== 'string' ||
|
|
48
|
+
!/^[A-Z]{3}$/.test(fee.currency) ||
|
|
49
|
+
(fee.withinMinutes !== undefined &&
|
|
50
|
+
(!Number.isSafeInteger(fee.withinMinutes) || fee.withinMinutes < 0 || fee.withinMinutes > 525_600))) {
|
|
51
|
+
throw new TypeError('Slotlock booking rule cancellationFee needs amountMinor (a whole 0-1,000,000,000,000), currency (ISO 4217, like EUR) and optionally withinMinutes (0-525600)');
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
amountMinor: fee.amountMinor,
|
|
55
|
+
currency: fee.currency,
|
|
56
|
+
...(fee.withinMinutes !== undefined ? { withinMinutes: fee.withinMinutes } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The cutoff rule a cancellation or move of a booking starting at `start`, made at `now`, breaks,
|
|
61
|
+
* or null (RFC 0004 B5).
|
|
62
|
+
*/
|
|
63
|
+
export function cutoffRuleBroken(rules, kind, start, now) {
|
|
64
|
+
const minutes = kind === 'cancel' ? rules?.cancelCutoffMinutes : rules?.rescheduleCutoffMinutes;
|
|
65
|
+
if (minutes === undefined)
|
|
66
|
+
return null;
|
|
67
|
+
return now.getTime() > start.getTime() - minutes * MINUTE_MS ? (kind === 'cancel' ? 'cancel_cutoff' : 'reschedule_cutoff') : null;
|
|
68
|
+
}
|
|
69
|
+
/** The fee a cancellation of a booking starting at `start`, made at `now`, records, or null. */
|
|
70
|
+
export function cancellationFeeDue(rules, start, now) {
|
|
71
|
+
const fee = rules?.cancellationFee;
|
|
72
|
+
if (!fee)
|
|
73
|
+
return null;
|
|
74
|
+
if (fee.withinMinutes !== undefined && now.getTime() < start.getTime() - fee.withinMinutes * MINUTE_MS)
|
|
75
|
+
return null;
|
|
76
|
+
return fee;
|
|
77
|
+
}
|
|
78
|
+
/** `allowedDurations` from untrusted input, or a TypeError. */
|
|
79
|
+
function parseAllowedDurations(value) {
|
|
80
|
+
if (!Array.isArray(value) ||
|
|
81
|
+
value.length < 1 ||
|
|
82
|
+
value.length > SLOTLOCK_MAX_ALLOWED_DURATIONS ||
|
|
83
|
+
!value.every((length) => Number.isSafeInteger(length) && length >= 1 && length <= 525_600) ||
|
|
84
|
+
new Set(value).size !== value.length) {
|
|
85
|
+
throw new TypeError(`Slotlock booking rule allowedDurations must be 1-${SLOTLOCK_MAX_ALLOWED_DURATIONS} different whole minutes, each 1-525600`);
|
|
86
|
+
}
|
|
87
|
+
return [...value].sort((a, b) => a - b);
|
|
88
|
+
}
|
|
89
|
+
/** `pickupTimes` or `returnTimes` from untrusted input, or a TypeError. */
|
|
90
|
+
function parseDayWindows(key, value) {
|
|
91
|
+
const valid = Array.isArray(value) &&
|
|
92
|
+
value.length >= 1 &&
|
|
93
|
+
value.length <= SLOTLOCK_MAX_DAY_WINDOWS &&
|
|
94
|
+
value.every((window) => typeof window === 'object' &&
|
|
95
|
+
window !== null &&
|
|
96
|
+
!Array.isArray(window) &&
|
|
97
|
+
Object.keys(window).every((name) => name === 'startMinutes' || name === 'durationMinutes') &&
|
|
98
|
+
Number.isSafeInteger(window.startMinutes) &&
|
|
99
|
+
window.startMinutes >= 0 &&
|
|
100
|
+
window.startMinutes < DAY_MINUTES &&
|
|
101
|
+
Number.isSafeInteger(window.durationMinutes) &&
|
|
102
|
+
window.durationMinutes >= 1 &&
|
|
103
|
+
window.durationMinutes <= DAY_MINUTES);
|
|
104
|
+
if (!valid) {
|
|
105
|
+
throw new TypeError(`Slotlock booking rule ${key} must be 1-${SLOTLOCK_MAX_DAY_WINDOWS} windows of startMinutes (0-1439) and durationMinutes (1-1440)`);
|
|
106
|
+
}
|
|
107
|
+
return value.map((window) => ({
|
|
108
|
+
startMinutes: window.startMinutes,
|
|
109
|
+
durationMinutes: window.durationMinutes,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/** Refuse durations that leave no length a booking may have (a TypeError naming them). */
|
|
113
|
+
function checkDurations(rules) {
|
|
114
|
+
const min = rules.minDurationMinutes ?? 1;
|
|
115
|
+
const max = rules.maxDurationMinutes ?? 525_600;
|
|
116
|
+
if (min > max)
|
|
117
|
+
throw new TypeError('Slotlock booking rule minDurationMinutes must not exceed maxDurationMinutes');
|
|
118
|
+
if (rules.allowedDurations && !rules.allowedDurations.some((length) => length >= min && length <= max)) {
|
|
119
|
+
throw new TypeError('Slotlock booking rule allowedDurations has no length within the minimum and maximum');
|
|
120
|
+
}
|
|
121
|
+
if (rules.defaultDurationMinutes !== undefined && durationRuleBroken(rules, rules.defaultDurationMinutes) !== null) {
|
|
122
|
+
throw new TypeError('Slotlock booking rule defaultDurationMinutes must be a length the other rules allow');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** The duration rule a booking of `minutes` breaks, or null. */
|
|
126
|
+
export function durationRuleBroken(rules, minutes) {
|
|
127
|
+
if (!rules)
|
|
128
|
+
return null;
|
|
129
|
+
if (rules.minDurationMinutes !== undefined && minutes < rules.minDurationMinutes)
|
|
130
|
+
return 'min_duration';
|
|
131
|
+
if (rules.maxDurationMinutes !== undefined && minutes > rules.maxDurationMinutes)
|
|
132
|
+
return 'max_duration';
|
|
133
|
+
if (rules.allowedDurations !== undefined && !rules.allowedDurations.includes(minutes))
|
|
134
|
+
return 'duration_not_allowed';
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
16
137
|
/** Booking rules from untrusted input, or a TypeError naming the field that is wrong. */
|
|
17
138
|
export function parseBookingRules(value) {
|
|
18
139
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
@@ -21,10 +142,20 @@ export function parseBookingRules(value) {
|
|
|
21
142
|
const input = value;
|
|
22
143
|
const rules = {};
|
|
23
144
|
for (const key of Object.keys(input)) {
|
|
24
|
-
if (!(key in LIMITS) && key !== 'enforceHours' && key !== 'requireCertain') {
|
|
145
|
+
if (!(key in LIMITS) && key !== 'enforceHours' && key !== 'requireCertain' && key !== 'hoursApply' && !LIST_RULES.has(key)) {
|
|
25
146
|
throw new TypeError(`Slotlock booking rules have no ${key}`);
|
|
26
147
|
}
|
|
27
148
|
}
|
|
149
|
+
if (input.hoursApply !== undefined)
|
|
150
|
+
rules.hoursApply = parseHoursApply(input.hoursApply);
|
|
151
|
+
if (input.allowedDurations !== undefined)
|
|
152
|
+
rules.allowedDurations = parseAllowedDurations(input.allowedDurations);
|
|
153
|
+
if (input.cancellationFee !== undefined)
|
|
154
|
+
rules.cancellationFee = parseCancellationFee(input.cancellationFee);
|
|
155
|
+
if (input.pickupTimes !== undefined)
|
|
156
|
+
rules.pickupTimes = parseDayWindows('pickupTimes', input.pickupTimes);
|
|
157
|
+
if (input.returnTimes !== undefined)
|
|
158
|
+
rules.returnTimes = parseDayWindows('returnTimes', input.returnTimes);
|
|
28
159
|
for (const [key, [min, max]] of Object.entries(LIMITS)) {
|
|
29
160
|
const field = input[key];
|
|
30
161
|
if (field === undefined)
|
|
@@ -51,6 +182,7 @@ export function parseBookingRules(value) {
|
|
|
51
182
|
rules.stepOffsetMinutes >= rules.slotStepMinutes) {
|
|
52
183
|
throw new TypeError('Slotlock booking rule stepOffsetMinutes must be less than slotStepMinutes');
|
|
53
184
|
}
|
|
185
|
+
checkDurations(rules);
|
|
54
186
|
return rules;
|
|
55
187
|
}
|
|
56
188
|
/**
|
|
@@ -73,6 +205,27 @@ export function parseEffectiveBookingRules(value) {
|
|
|
73
205
|
dropped.push(key);
|
|
74
206
|
continue;
|
|
75
207
|
}
|
|
208
|
+
if (key === 'hoursApply') {
|
|
209
|
+
if (field === 'whole' || field === 'ends')
|
|
210
|
+
rules.hoursApply = field;
|
|
211
|
+
else
|
|
212
|
+
dropped.push(key);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (LIST_RULES.has(key)) {
|
|
216
|
+
try {
|
|
217
|
+
if (key === 'allowedDurations')
|
|
218
|
+
rules.allowedDurations = parseAllowedDurations(field);
|
|
219
|
+
else if (key === 'cancellationFee')
|
|
220
|
+
rules.cancellationFee = parseCancellationFee(field);
|
|
221
|
+
else
|
|
222
|
+
rules[key] = parseDayWindows(key, field);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
dropped.push(key);
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
76
229
|
const limits = LIMITS[key];
|
|
77
230
|
if (limits === undefined ||
|
|
78
231
|
typeof field !== 'number' ||
|
|
@@ -84,6 +237,13 @@ export function parseEffectiveBookingRules(value) {
|
|
|
84
237
|
}
|
|
85
238
|
rules[key] = field;
|
|
86
239
|
}
|
|
240
|
+
// Levels are validated alone: a combination that leaves no length refuses every booking.
|
|
241
|
+
try {
|
|
242
|
+
checkDurations(rules);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
dropped.push('durations');
|
|
246
|
+
}
|
|
87
247
|
return { rules, dropped };
|
|
88
248
|
}
|
|
89
249
|
/**
|
|
@@ -138,9 +298,35 @@ export function nextStepStart(from, stepMinutes, timezone, offsetMinutes = 0) {
|
|
|
138
298
|
}
|
|
139
299
|
return candidate;
|
|
140
300
|
}
|
|
301
|
+
/** The local day an instant falls on in `timezone`, as days since 1970-01-01. */
|
|
302
|
+
function localDay(instant, timezone) {
|
|
303
|
+
const local = instant.getTime() + zonedOffsetMinutes(instant, timezone) * MINUTE_MS;
|
|
304
|
+
return Math.floor(local / (DAY_MINUTES * MINUTE_MS));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Whether a booking runs from one local day into another (a hire): its last moment falls on a later
|
|
308
|
+
* local day than its start. One that ends at midnight stays on its day.
|
|
309
|
+
*/
|
|
310
|
+
export function isMultiDay(interval, timezone) {
|
|
311
|
+
return localDay(new Date(interval.end.getTime() - 1), timezone) > localDay(interval.start, timezone);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Whether `instant` lies in one of the windows of its local day: `[start, start + length)`, or with
|
|
315
|
+
* `closed` (a return) up to and including the window's end.
|
|
316
|
+
*/
|
|
317
|
+
export function withinDayWindows(instant, windows, timezone, closed = false) {
|
|
318
|
+
const minutes = localMinutes(instant, timezone);
|
|
319
|
+
return windows.some((window) => {
|
|
320
|
+
const into = (((minutes - window.startMinutes) % DAY_MINUTES) + DAY_MINUTES) % DAY_MINUTES;
|
|
321
|
+
return closed ? into <= window.durationMinutes : into < window.durationMinutes;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
141
324
|
/**
|
|
142
|
-
* The rule a booking of `interval` made at `now` breaks, or null. `hours` (the resource's own,
|
|
143
|
-
* the default) are read only when enforced.
|
|
325
|
+
* The rule a booking of `interval` made at `now` breaks, or null. `hours` (the resource's own, else
|
|
326
|
+
* inherited, else the default) and `exceptions` are read only when enforced. A hire (a booking from
|
|
327
|
+
* one local day to another) under `pickupTimes` or `returnTimes` starts and ends in them, and under
|
|
328
|
+
* `enforceHours` lies inside the hours as `hoursApply` says: wholly, or only at its start and end
|
|
329
|
+
* (the resource being out between).
|
|
144
330
|
*/
|
|
145
331
|
export function bookingRuleBroken(params) {
|
|
146
332
|
const { rules, interval, now } = params;
|
|
@@ -158,18 +344,37 @@ export function bookingRuleBroken(params) {
|
|
|
158
344
|
!onStep(interval.start, rules.slotStepMinutes, params.timezone, stepOffset(rules))) {
|
|
159
345
|
return 'slot_step';
|
|
160
346
|
}
|
|
347
|
+
const duration = durationRuleBroken(rules, (interval.end.getTime() - start) / MINUTE_MS);
|
|
348
|
+
if (duration)
|
|
349
|
+
return duration;
|
|
350
|
+
const multiDay = isMultiDay(interval, params.timezone);
|
|
351
|
+
if (multiDay && rules.pickupTimes && !withinDayWindows(interval.start, rules.pickupTimes, params.timezone)) {
|
|
352
|
+
return 'pickup_time';
|
|
353
|
+
}
|
|
354
|
+
if (multiDay && rules.returnTimes && !withinDayWindows(interval.end, rules.returnTimes, params.timezone, true)) {
|
|
355
|
+
return 'return_time';
|
|
356
|
+
}
|
|
357
|
+
// A hire the hours hold only at its ends.
|
|
358
|
+
const hire = multiDay && hoursApplyOf(rules) === 'ends';
|
|
161
359
|
if (rules.enforceHours === true) {
|
|
162
|
-
const open =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
360
|
+
const open = (window) => openWindows({
|
|
361
|
+
hours: params.hours,
|
|
362
|
+
...(params.exceptions !== undefined ? { exceptions: params.exceptions } : {}),
|
|
363
|
+
timezone: params.timezone,
|
|
364
|
+
window,
|
|
365
|
+
});
|
|
366
|
+
if (hire) {
|
|
367
|
+
// Open at the start, and open up to the end: the hours a hire is collected and returned in.
|
|
368
|
+
const minute = MINUTE_MS;
|
|
369
|
+
const atStart = { start: interval.start, end: new Date(start + minute) };
|
|
370
|
+
const atEnd = { start: new Date(interval.end.getTime() - minute), end: interval.end };
|
|
371
|
+
if (!coveredByOpenWindows(open(atStart), atStart) || !coveredByOpenWindows(open(atEnd), atEnd)) {
|
|
372
|
+
return 'outside_hours';
|
|
373
|
+
}
|
|
170
374
|
}
|
|
171
|
-
if (
|
|
375
|
+
else if (!coveredByOpenWindows(open(interval), interval)) {
|
|
172
376
|
return 'outside_hours';
|
|
377
|
+
}
|
|
173
378
|
}
|
|
174
379
|
return null;
|
|
175
380
|
}
|
|
@@ -179,6 +384,9 @@ export function bookingRuleBroken(params) {
|
|
|
179
384
|
*/
|
|
180
385
|
export function bookingSearch(params) {
|
|
181
386
|
const { rules, window, now } = params;
|
|
387
|
+
if (params.durationMs !== undefined && durationRuleBroken(rules, params.durationMs / MINUTE_MS) !== null) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
182
390
|
let start = window.start.getTime();
|
|
183
391
|
const end = window.end.getTime();
|
|
184
392
|
if (rules?.minNoticeMinutes !== undefined) {
|
|
@@ -188,7 +396,27 @@ export function bookingSearch(params) {
|
|
|
188
396
|
const bufferBeforeMs = bookingBufferBeforeMs(rules);
|
|
189
397
|
const step = rules?.slotStepMinutes;
|
|
190
398
|
const offset = stepOffset(rules);
|
|
191
|
-
const
|
|
399
|
+
const stepped = step === undefined ? undefined : (from) => nextStepStart(from, step, params.timezone, offset);
|
|
400
|
+
// A slot that runs from one local day into another starts and ends in the pick-up and return
|
|
401
|
+
// times: candidates move on a minute (or a step) at a time, for at most two days.
|
|
402
|
+
const durationMs = params.durationMs;
|
|
403
|
+
const hireRules = rules && (rules.pickupTimes !== undefined || rules.returnTimes !== undefined) ? rules : undefined;
|
|
404
|
+
const toStep = hireRules === undefined || durationMs === undefined
|
|
405
|
+
? stepped
|
|
406
|
+
: (from) => {
|
|
407
|
+
let candidate = stepped ? stepped(from) : new Date(Math.ceil(from.getTime() / MINUTE_MS) * MINUTE_MS);
|
|
408
|
+
for (let tries = 0; tries <= 2 * DAY_MINUTES; tries++) {
|
|
409
|
+
const slot = { start: candidate, end: new Date(candidate.getTime() + durationMs) };
|
|
410
|
+
if (!isMultiDay(slot, params.timezone) ||
|
|
411
|
+
((!hireRules.pickupTimes || withinDayWindows(slot.start, hireRules.pickupTimes, params.timezone)) &&
|
|
412
|
+
(!hireRules.returnTimes || withinDayWindows(slot.end, hireRules.returnTimes, params.timezone, true)))) {
|
|
413
|
+
return candidate;
|
|
414
|
+
}
|
|
415
|
+
const next = new Date(Math.floor(candidate.getTime() / MINUTE_MS) * MINUTE_MS + MINUTE_MS);
|
|
416
|
+
candidate = stepped ? stepped(next) : next;
|
|
417
|
+
}
|
|
418
|
+
return new Date(end + 1);
|
|
419
|
+
};
|
|
192
420
|
const searched = { start: new Date(start), end: new Date(end) };
|
|
193
421
|
if (rules?.maxAdvanceDays !== undefined) {
|
|
194
422
|
const latestStart = now.getTime() + rules.maxAdvanceDays * DAY_MINUTES * MINUTE_MS;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** How a step reads while it runs, once it has, and when it could not. */
|
|
2
|
+
export type SlotlockAssistantStepState = 'run' | 'ok' | 'bad';
|
|
3
|
+
/** A tool step in plain words: "Checking availability…", "Checked availability", "Couldn't check availability". */
|
|
4
|
+
export declare function slotlockAssistantStepLabel(name: string, title: string, state: SlotlockAssistantStepState, write?: boolean): string;
|
|
5
|
+
/** The verb a proposed change's card is approved with, and whether it removes something. */
|
|
6
|
+
export declare function slotlockAssistantActionVerb(operation: string): {
|
|
7
|
+
verb: string;
|
|
8
|
+
busy: string;
|
|
9
|
+
done: string;
|
|
10
|
+
danger: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** The chat's styles: one centred column, the composer docked under it, quiet steps and cards. */
|
|
13
|
+
export declare const SLOTLOCK_ASSISTANT_CSS = "\n.assistant-page { display: grid; }\n.assistant { position: relative; display: flex; flex-direction: column; width: min(100%, 720px); margin: 0 auto; min-height: calc(100dvh - 180px); }\n.assistant .thread { flex: 1; }\n.assistant[data-empty] { justify-content: center; padding-bottom: 12vh; }\n.assistant[data-empty] .thread { display: none; }\n.assistant[data-empty] .dock { order: 1; position: static; padding: 0; background: none; }\n.assistant[data-empty] .assistant-start { order: 2; padding-top: var(--space-3); }\n.assistant:not([data-empty]) .assistant-start { display: none; }\n.chips { display: flex; flex-wrap: wrap; justify-content: center; gap: var(--space-2); }\n.chips form { margin: 0; }\n.chip { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-full); border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); font: inherit; font-size: var(--text-md); cursor: pointer; transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease); }\n.chip:hover { background: var(--hover); border-color: var(--line-strong); color: var(--ink); }\n.thread { list-style: none; margin: 0; padding: var(--space-2) 0 var(--space-6); display: grid; gap: var(--space-6); align-content: start; }\n.turn { margin: 0; min-width: 0; }\n.turn.mine { justify-self: end; max-width: 80%; }\n.bubble { padding: 10px 14px; border-radius: 18px; background: var(--sunken); color: var(--ink); font-size: var(--text-lg); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }\n.turn.bot { justify-self: stretch; }\n.prose { max-width: 68ch; color: var(--ink); font-size: var(--text-lg); line-height: 1.6; overflow-wrap: anywhere; }\n.prose > :first-child { margin-top: 0; }\n.prose > :last-child { margin-bottom: 0; }\n.prose p { margin: 0 0 var(--space-3); color: inherit; }\n.prose strong { font-weight: 600; }\n.prose h4 { margin: var(--space-4) 0 var(--space-2); font-size: var(--text-lg); font-weight: 600; }\n.prose ul, .prose ol { margin: 0 0 var(--space-3); padding-left: 20px; }\n.prose li { margin: 4px 0; }\n.prose li > ul, .prose li > ol { margin: 4px 0 0; }\n.prose a { color: var(--accent); text-underline-offset: 2px; }\n.prose code { font: 400 12.5px/1.4 var(--mono); background: var(--sunken); border-radius: 4px; padding: 1px 5px; }\n.prose pre { margin: 0 0 var(--space-3); padding: var(--space-3); background: var(--sunken); border: 1px solid var(--line); border-radius: var(--radius); overflow-x: auto; }\n.prose pre code { background: none; padding: 0; font-size: 12.5px; }\n.prose blockquote { margin: 0 0 var(--space-3); padding-left: var(--space-3); border-left: 2px solid var(--line-strong); color: var(--ink-2); }\n.prose hr { border: 0; border-top: 1px solid var(--line); margin: var(--space-4) 0; }\n.md-table { overflow-x: auto; margin: 0 0 var(--space-3); }\n.md-table table { border-collapse: collapse; font-size: var(--text-md); min-width: 50%; }\n.md-table table { width: auto; }\n.md-table th { font: 600 var(--text-md)/1.4 var(--font); text-transform: none; letter-spacing: 0; background: none; text-align: left; color: var(--ink-2); padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line); white-space: nowrap; }\n.md-table td { padding: 6px 16px 6px 0; border-bottom: 1px solid var(--line-soft); vertical-align: top; }\n.md-table tr:last-child td { border-bottom: 0; }\n.change details.more { margin: 0; }\n.change details.more > summary { cursor: pointer; color: var(--ink-3); font-size: var(--text-sm); list-style: none; }\n.change details.more > summary::-webkit-details-marker { display: none; }\n.change details.more > summary:hover { color: var(--ink-2); }\n.change details.more[open] > summary { margin-bottom: var(--space-2); }\n.turn.steps { justify-self: start; }\ndetails.steps > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: var(--ink-3); font-size: var(--text-md); padding: 2px 0; }\ndetails.steps > summary::-webkit-details-marker { display: none; }\ndetails.steps > summary::after { content: \"\"; width: 6px; height: 6px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(-45deg); transition: transform var(--fast) var(--ease); margin-left: 2px; }\ndetails.steps[open] > summary::after { transform: rotate(45deg); }\ndetails.steps > summary:hover { color: var(--ink-2); }\ndetails.steps.has-bad > summary::before { content: \"\"; width: 6px; height: 6px; border-radius: 50%; background: var(--bad); }\n.step-list { list-style: none; margin: var(--space-2) 0 0; padding: 0 0 0 var(--space-3); border-left: 1px solid var(--line); display: grid; gap: 6px; }\n.step { display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }\n.step::before { content: \"\"; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--good); transform: translateY(-1px); }\n.step.bad::before { background: var(--bad); }\n.step.run::before { background: var(--ink-3); }\n.step .why { color: var(--ink-3); font-size: var(--text-sm); }\n.live-steps { display: grid; gap: 6px; }\n.live-steps .step.run, .thinking { background: linear-gradient(90deg, var(--ink-3) 0%, var(--ink) 50%, var(--ink-3) 100%); background-size: 200% 100%; -webkit-background-clip: text; background-clip: text; color: transparent; animation: assistant-shimmer 1.6s linear infinite; }\n.thinking { font-size: var(--text-md); justify-self: start; }\n@keyframes assistant-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n.prose.streaming > :last-child::after { content: \"\"; display: inline-block; width: 8px; height: 8px; margin-left: 4px; border-radius: 50%; background: var(--ink-3); vertical-align: baseline; animation: assistant-pulse 1s ease-in-out infinite; }\n@keyframes assistant-pulse { 50% { opacity: 0.25; } }\n.turn.note { color: var(--ink-3); font-size: var(--text-md); }\n.turn.error { display: flex; align-items: center; justify-content: space-between; gap: var(--space-3); padding: 10px 12px; border: 1px solid var(--bad-line); background: var(--bad-wash); border-radius: var(--radius); font-size: var(--text-md); color: var(--ink); }\n.turn.error::before { content: \"\"; flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--bad); }\n.turn.error > span { flex: 1; }\n.turn.error form, .turn.note form { margin: 0; }\n.text-button { appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: var(--text-md); color: var(--accent); cursor: pointer; }\n.text-button:hover { text-decoration: underline; text-underline-offset: 2px; }\n.turn.action { justify-self: start; width: min(100%, 560px); }\n.change { padding: var(--space-4) var(--space-5); background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-lg); display: grid; gap: var(--space-3); }\n.change-title { margin: 0; font-size: var(--text-lg); font-weight: 600; color: var(--ink); overflow-wrap: anywhere; }\n.change .facts { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px var(--space-4); margin: 0; font-size: var(--text-md); }\n.change .facts dt { color: var(--ink-3); font-size: var(--text-sm); padding-top: 1px; }\n.change .facts dd { margin: 0; color: var(--ink); overflow-wrap: anywhere; }\n.change .facts dd pre { margin: 0; white-space: pre-wrap; font: 400 12px/1.5 var(--mono); }\n.change .facts .was { color: var(--ink-3); text-decoration: line-through; }\n.change-buttons { display: flex; gap: var(--space-2); flex-wrap: wrap; }\n.change-buttons form { margin: 0; }\n.change-buttons button { height: 32px; }\nbutton.danger { background: var(--bad); color: var(--bad-ink); border-color: var(--bad); }\nbutton.danger:hover { filter: brightness(1.08); }\nbutton.ghost { background: transparent; border-color: transparent; color: var(--ink-2); }\nbutton.ghost:hover { background: var(--hover); color: var(--ink); }\n.receipt { margin: 0; display: flex; align-items: baseline; gap: var(--space-2); font-size: var(--text-md); color: var(--ink-2); }\n.receipt::before { content: \"\"; flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--ink-3); transform: translateY(-1px); }\n.receipt.ok { color: var(--ink); }\n.receipt.ok::before { background: var(--good); }\n.receipt.bad::before { background: var(--bad); }\n.receipt .why { color: var(--ink-3); }\n.dock { position: sticky; bottom: 0; padding: var(--space-6) 0 var(--space-6); background: linear-gradient(to bottom, transparent, var(--canvas) 24px); z-index: 2; }\n.composer { display: grid; margin: 0; background: var(--surface); border: 1px solid var(--line); border-radius: 16px; transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease); }\n.composer:focus-within { border-color: var(--line-strong); box-shadow: 0 0 0 3px var(--ring); }\n.composer textarea { display: block; width: 100%; min-height: 26px; max-height: 200px; resize: none; border: 0; outline: 0; background: transparent; padding: 14px 16px 0; color: var(--ink); font: inherit; font-size: var(--text-lg); line-height: 1.5; box-shadow: none; }\n.composer textarea:focus { box-shadow: none; border: 0; }\n.composer-bar { display: flex; align-items: center; justify-content: flex-end; gap: var(--space-2); padding: var(--space-2); }\n.composer-bar .count { margin-right: auto; padding-left: var(--space-2); font-size: var(--text-sm); color: var(--ink-3); }\n.composer-bar .count.over { color: var(--warn); }\n.send { display: inline-grid; place-items: center; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 0; background: var(--button); color: var(--button-ink); cursor: pointer; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }\n.send:hover { background: var(--button-hover); }\n.send:disabled { opacity: 0.4; cursor: default; }\n.send svg { width: 16px; height: 16px; }\n.send .stop { display: none; }\n.assistant.streaming .send .arrow { display: none; }\n.assistant.streaming .send .stop { display: block; }\n.jump { position: absolute; left: 50%; transform: translate(-50%, 0); top: -8px; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); display: grid; place-items: center; cursor: pointer; box-shadow: var(--pop-shadow); }\n.jump[hidden] { display: none; }\n.new-chat svg, .chats-link svg { width: 14px; height: 14px; }\n.assistant-page[data-empty] .new-chat { display: none; }\n.assistant-actions { display: flex; gap: var(--space-2); }\n.chats-link { display: none; }\n/* Chats: the member's conversations beside the one shown, most recent first, grouped by day. */\n.assistant-shell { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: var(--space-6); align-items: start; }\n/* It scrolls with the page: a column that scrolled on its own would clip a row's menu. */\n.chats { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }\n.chats-head { display: flex; align-items: center; justify-content: space-between; min-height: 36px; }\n.chats-head h2 { margin: 0; font-size: var(--text-md); font-weight: 600; color: var(--ink-2); }\n.icon-button { display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm); color: var(--ink-2); transition: background var(--fast) var(--ease), color var(--fast) var(--ease); }\n.icon-button:hover { background: var(--hover); color: var(--ink); text-decoration: none; }\n.icon-button svg { width: 15px; height: 15px; }\n.chats-filter { height: 32px; font-size: var(--text-md); }\n.chats-filter[hidden] { display: none; }\n/* Sentence case, not the dashboard's uppercase h3. */\n.chat-group { margin: var(--space-3) 0 2px; padding: 0 var(--space-2); font-family: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1.5; text-transform: none; letter-spacing: 0; color: var(--ink-3); }\n.chat-group[hidden] { display: none; }\n.chat-list { min-width: 0; }\n/* minmax(0, 1fr): a long title is cut with an ellipsis, never widening the column. */\n.chat-rows { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; }\n.chat-row { position: relative; display: flex; align-items: center; border-radius: var(--radius); transition: background var(--fast) var(--ease); }\n.chat-row[hidden] { display: none; }\n.chat-row:hover, .chat-row:focus-within, .chat-row.current { background: var(--hover); }\n.chat-link { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--space-2); padding: 7px 4px 7px var(--space-2); color: var(--ink-2); font-size: var(--text-md); }\n.chat-link:hover { color: var(--ink); text-decoration: none; }\n.chat-row.current .chat-link { color: var(--ink); font-weight: 500; }\n.chat-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.chat-when { flex: none; font-size: var(--text-xs); color: var(--ink-3); font-variant-numeric: tabular-nums; }\n.chat-menu { position: relative; flex: none; }\n.chat-menu summary { list-style: none; display: grid; place-items: center; width: 28px; height: 28px; margin-right: 2px; border-radius: var(--radius-sm); color: var(--ink-3); cursor: pointer; opacity: 0; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }\n.chat-menu summary::-webkit-details-marker { display: none; }\n.chat-row:hover .chat-menu summary, .chat-row:focus-within .chat-menu summary, .chat-row.current .chat-menu summary, .chat-menu[open] summary { opacity: 1; }\n.chat-menu summary:hover, .chat-menu[open] summary { background: var(--line-soft); color: var(--ink); }\n.chat-menu summary svg { width: 14px; height: 14px; }\n.chat-menu .pop { z-index: 20; width: 260px; display: grid; gap: var(--space-2); }\n.chat-menu .pop form { display: grid; gap: var(--space-2); margin: 0; }\n.chat-menu .pop form + form { padding-top: var(--space-2); border-top: 1px solid var(--line-soft); }\n.chat-menu .pop button.danger { background: none; border-color: transparent; box-shadow: none; color: var(--bad); }\n.chat-menu .pop button.danger:hover { background: var(--bad-wash); }\n.chat-rename input { width: 100%; min-width: 0; height: 34px; }\n.chats-empty { padding: var(--space-2); font-size: var(--text-md); }\n.chats-page .chats { max-width: 560px; }\n.chats-page .chats-head { display: none; }\n.chats-page .chat-menu summary { opacity: 1; }\n@media (max-width: 1100px) {\n .assistant-shell { grid-template-columns: minmax(0, 1fr); }\n .assistant-shell > .chats { display: none; }\n .chats-link { display: inline-flex; }\n}\n@media (hover: none) {\n .chat-menu summary { opacity: 1; }\n}\n@media (prefers-reduced-motion: reduce) {\n .live-steps .step.run, .thinking { animation: none; color: var(--ink-3); background: none; }\n .prose.streaming > :last-child::after { animation: none; }\n}\n@media (max-width: 760px) {\n .assistant { min-height: calc(100dvh - 140px); }\n .assistant[data-empty] { justify-content: flex-end; padding-bottom: 0; }\n .assistant[data-empty] .assistant-start { order: 1; padding: 0 0 var(--space-3); }\n .assistant[data-empty] .dock { order: 2; position: sticky; padding-bottom: max(12px, env(safe-area-inset-bottom)); }\n .chips { flex-wrap: nowrap; justify-content: flex-start; overflow-x: auto; max-width: 100%; padding-bottom: 2px; scrollbar-width: none; }\n .chips::-webkit-scrollbar { display: none; }\n .chip { flex: none; }\n .turn.mine { max-width: 88%; }\n .md-table table { display: table; }\n .md-table thead { display: table-header-group; }\n .md-table tbody { display: table-row-group; }\n .md-table tr { display: table-row; padding: 0; border: 0; }\n .md-table th, .md-table td { display: table-cell; padding: 6px 12px 6px 0; }\n .md-table td { border-bottom: 1px solid var(--line-soft); }\n .composer textarea { font-size: 16px; }\n .dock { padding-bottom: max(12px, env(safe-area-inset-bottom)); }\n .change { padding: var(--space-4); }\n .change .facts { grid-template-columns: minmax(0, 1fr); gap: 2px; }\n .change .facts dd { margin-bottom: var(--space-2); }\n .change-buttons form { flex: 1; }\n .change-buttons button { width: 100%; height: 40px; }\n}\n";
|
|
14
|
+
/**
|
|
15
|
+
* The browser side of the chat, appended to app.js: Enter sends, the box grows, a write chip fills
|
|
16
|
+
* the box instead of sending, and a reply streams in from `<root>/assistant/stream` (newline-
|
|
17
|
+
* delimited JSON: text deltas, steps, then the turn as the server renders it). Without it the
|
|
18
|
+
* page still works: every form posts and the page reloads at the latest message.
|
|
19
|
+
* `markdownSource` is renderSlotlockMarkdown's source, so a reply renders as it will once stored.
|
|
20
|
+
*/
|
|
21
|
+
export declare function slotlockAssistantScript(markdownSource: string): string;
|