@rightkit/workspace-ui 0.1.0 → 0.2.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +24 -14
  3. package/dist/client.d.ts +190 -0
  4. package/dist/client.js +2 -0
  5. package/dist/hooks/useDrive.d.ts +26 -0
  6. package/dist/hooks/useDrive.js +158 -0
  7. package/dist/host.d.ts +109 -0
  8. package/dist/host.js +33 -0
  9. package/dist/icons.d.ts +34 -0
  10. package/dist/icons.js +33 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.js +11 -2
  13. package/dist/lib/avatar.d.ts +4 -0
  14. package/dist/lib/avatar.js +13 -0
  15. package/dist/lib/calendarUtils.d.ts +11 -0
  16. package/dist/lib/calendarUtils.js +42 -0
  17. package/dist/views/CalendarEventFormModal.d.ts +1 -1
  18. package/dist/views/CalendarEventFormModal.js +1 -1
  19. package/dist/views/CalendarEventPopover.d.ts +1 -1
  20. package/dist/views/CalendarEventPopover.js +3 -2
  21. package/dist/views/CalendarGrids.d.ts +1 -1
  22. package/dist/views/CalendarGrids.js +1 -1
  23. package/dist/views/CalendarView.js +34 -35
  24. package/dist/views/ContactsView.js +24 -27
  25. package/dist/views/DriveFileMenu.d.ts +2 -2
  26. package/dist/views/DriveFileMenu.js +3 -3
  27. package/dist/views/DrivePreviewModal.d.ts +2 -2
  28. package/dist/views/DrivePreviewModal.js +4 -4
  29. package/dist/views/DriveView.js +23 -24
  30. package/dist/views/MeetView.js +11 -11
  31. package/dist/views/TasksView.js +29 -29
  32. package/dist/views/driveFileGlyph.d.ts +3 -2
  33. package/dist/views/nav/CalendarNav.d.ts +1 -0
  34. package/dist/views/nav/CalendarNav.js +93 -0
  35. package/dist/views/nav/ContactsNav.d.ts +1 -0
  36. package/dist/views/nav/ContactsNav.js +22 -0
  37. package/dist/views/nav/DriveNav.d.ts +1 -0
  38. package/dist/views/nav/DriveNav.js +44 -0
  39. package/dist/views/nav/TasksNav.d.ts +1 -0
  40. package/dist/views/nav/TasksNav.js +22 -0
  41. package/package.json +23 -9
package/dist/index.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export * from "./client.js";
2
+ export * from "./host.js";
3
+ export * from "./icons.js";
4
+ export * from "./hooks/useDrive.js";
5
+ export * from "./lib/avatar.js";
6
+ export * from "./lib/calendarUtils.js";
1
7
  export * from "./lib/vcard.js";
2
8
  export * from "./views/CalendarEventFormModal.js";
3
9
  export * from "./views/CalendarEventPopover.js";
@@ -11,3 +17,7 @@ export * from "./views/DriveView.js";
11
17
  export * from "./views/MeetView.js";
12
18
  export * from "./views/TasksView.js";
13
19
  export * from "./views/driveFileGlyph.js";
20
+ export * from "./views/nav/CalendarNav.js";
21
+ export * from "./views/nav/ContactsNav.js";
22
+ export * from "./views/nav/DriveNav.js";
23
+ export * from "./views/nav/TasksNav.js";
package/dist/index.js CHANGED
@@ -1,5 +1,10 @@
1
- // Generated entry: every module whose exports do not collide with an earlier one.
2
- // Every module is also importable directly by subpath, e.g. `<package>/lib/foo`.
1
+ // The workspace views and navs, the host contract they render over, and the workspace data model.
2
+ export * from "./client.js";
3
+ export * from "./host.js";
4
+ export * from "./icons.js";
5
+ export * from "./hooks/useDrive.js";
6
+ export * from "./lib/avatar.js";
7
+ export * from "./lib/calendarUtils.js";
3
8
  export * from "./lib/vcard.js";
4
9
  export * from "./views/CalendarEventFormModal.js";
5
10
  export * from "./views/CalendarEventPopover.js";
@@ -13,3 +18,7 @@ export * from "./views/DriveView.js";
13
18
  export * from "./views/MeetView.js";
14
19
  export * from "./views/TasksView.js";
15
20
  export * from "./views/driveFileGlyph.js";
21
+ export * from "./views/nav/CalendarNav.js";
22
+ export * from "./views/nav/ContactsNav.js";
23
+ export * from "./views/nav/DriveNav.js";
24
+ export * from "./views/nav/TasksNav.js";
@@ -0,0 +1,4 @@
1
+ /** The first letter of a name or address, upper-cased; "?" for an empty one. */
2
+ export declare const initialOf: (s: string) => string;
3
+ /** Stable per-person avatar colour (Gmail-style coloured initials). */
4
+ export declare function colorFor(s: string): string;
@@ -0,0 +1,13 @@
1
+ // Coloured-initial avatars for people (contacts, senders). One copy: the mail views import them from here.
2
+ /** The first letter of a name or address, upper-cased; "?" for an empty one. */
3
+ export const initialOf = (s) => (s.trim()[0] || "?").toUpperCase();
4
+ const AVATAR_COLORS = [
5
+ "#C46A3D", "#6E8B6B", "#B07A84", "#7E8AA8", "#B59A52", "#8A6BA8", "#5E8C8C", "#A8675B",
6
+ ];
7
+ /** Stable per-person avatar colour (Gmail-style coloured initials). */
8
+ export function colorFor(s) {
9
+ let h = 0;
10
+ for (let i = 0; i < s.length; i++)
11
+ h = (h * 31 + s.charCodeAt(i)) >>> 0;
12
+ return AVATAR_COLORS[h % AVATAR_COLORS.length];
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { CalEvent } from "../client.js";
2
+ export declare const DAY = 86400000;
3
+ export declare const WEEKDAY_LABELS: string[];
4
+ /** RRULE BYDAY codes with their short labels, Monday first (the event form's repeat-on picker). */
5
+ export declare const WEEKDAYS: ReadonlyArray<readonly [string, string]>;
6
+ export declare const startOfDay: (ms: number) => number;
7
+ export declare const startOfWeek: (ms: number) => number;
8
+ export declare const sameDay: (a: number, b: number) => boolean;
9
+ export declare function rangeFor(cursor: number, view: "month" | "week" | "day"): [number, number];
10
+ export declare function eventDay(e: CalEvent): number;
11
+ export declare const hhmm: (s: string) => string;
@@ -0,0 +1,42 @@
1
+ export const DAY = 86400000;
2
+ export const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3
+ /** RRULE BYDAY codes with their short labels, Monday first (the event form's repeat-on picker). */
4
+ export const WEEKDAYS = [
5
+ ["MO", "Mon"],
6
+ ["TU", "Tue"],
7
+ ["WE", "Wed"],
8
+ ["TH", "Thu"],
9
+ ["FR", "Fri"],
10
+ ["SA", "Sat"],
11
+ ["SU", "Sun"],
12
+ ];
13
+ export const startOfDay = (ms) => {
14
+ const d = new Date(ms);
15
+ d.setHours(0, 0, 0, 0);
16
+ return d.getTime();
17
+ };
18
+ export const startOfWeek = (ms) => {
19
+ const d = new Date(startOfDay(ms));
20
+ return d.getTime() - d.getDay() * DAY;
21
+ };
22
+ export const sameDay = (a, b) => startOfDay(a) === startOfDay(b);
23
+ export function rangeFor(cursor, view) {
24
+ if (view === "day")
25
+ return [startOfDay(cursor), startOfDay(cursor) + DAY];
26
+ if (view === "week") {
27
+ const s = startOfWeek(cursor);
28
+ return [s, s + 7 * DAY];
29
+ }
30
+ const d = new Date(cursor);
31
+ const first = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
32
+ const gridStart = startOfWeek(first);
33
+ return [gridStart, gridStart + 42 * DAY];
34
+ }
35
+ export function eventDay(e) {
36
+ if (e.all_day) {
37
+ const [y, m, d] = e.start.slice(0, 10).split("-").map(Number);
38
+ return new Date(y, (m || 1) - 1, d || 1).getTime();
39
+ }
40
+ return startOfDay(new Date(e.start).getTime());
41
+ }
42
+ export const hhmm = (s) => new Date(s).toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
@@ -1,4 +1,4 @@
1
- import type { EventForm } from "@rightkit/mail-ui/store/types";
1
+ import type { EventForm } from "../client.js";
2
2
  type CalendarEventFormModalProps = {
3
3
  eventForm: EventForm;
4
4
  setEventForm: (form: EventForm | null) => void;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Dialog, Input, Checkbox, Select } from "@rightkit/react-shell/react";
3
- import { WEEKDAYS } from "@rightkit/mail-ui/lib/mail";
3
+ import { WEEKDAYS } from "../lib/calendarUtils.js";
4
4
  export function CalendarEventFormModal({ eventForm, setEventForm, freeBusyMsg, checkAvailability, submitEvent, }) {
5
5
  return (_jsx(Dialog, { title: eventForm.id ? "Edit event" : "New event", width: 460, onClose: () => setEventForm(null), children: _jsxs("div", { className: "ws-form", children: [_jsx(Input, { className: "event-title-input", placeholder: "Add title", autoFocus: true, value: eventForm.summary, onChange: (e) => setEventForm({ ...eventForm, summary: e.target.value }) }), _jsxs("label", { className: "ws-form-row", children: [_jsx("span", { children: "Start" }), _jsx(Input, { type: eventForm.allDay ? "date" : "datetime-local", value: eventForm.allDay ? eventForm.start.slice(0, 10) : eventForm.start, onChange: (e) => setEventForm({ ...eventForm, start: eventForm.allDay ? `${e.target.value}T09:00` : e.target.value }) })] }), _jsxs("label", { className: "ws-form-row", children: [_jsx("span", { children: "End" }), _jsx(Input, { type: eventForm.allDay ? "date" : "datetime-local", value: eventForm.allDay ? eventForm.end.slice(0, 10) : eventForm.end, onChange: (e) => setEventForm({ ...eventForm, end: eventForm.allDay ? `${e.target.value}T10:00` : e.target.value }) })] }), _jsx("div", { className: "label-toggle", children: _jsx(Checkbox, { label: "All day", checked: eventForm.allDay, onChange: (on) => setEventForm({ ...eventForm, allDay: on }) }) }), _jsx(Input, { placeholder: "Guests (comma-separated emails)", value: eventForm.attendees, onChange: (e) => setEventForm({ ...eventForm, attendees: e.target.value }) }), _jsxs("label", { className: "ws-form-row", children: [_jsx("span", { children: "Remind (min before)" }), _jsx(Input, { type: "number", style: { width: 80 }, value: eventForm.reminderMinutes, onChange: (e) => setEventForm({ ...eventForm, reminderMinutes: e.target.value }) })] }), _jsx("div", { className: "label-toggle", children: _jsx(Checkbox, { label: "Add Google Meet video call", checked: eventForm.addMeet, onChange: (on) => setEventForm({ ...eventForm, addMeet: on }) }) }), _jsxs("label", { className: "ws-form-row", children: [_jsx("span", { children: "Repeats" }), _jsx(Select, { label: "Repeats", value: eventForm.recurrence, options: [
6
6
  { value: "", label: "Does not repeat" },
@@ -1,4 +1,4 @@
1
- import { type CalEvent } from "@rightkit/mail-ui/lib/ipc";
1
+ import type { CalEvent } from "../client.js";
2
2
  type CalendarEventPopoverProps = {
3
3
  openEvent: CalEvent;
4
4
  setOpenEvent: (ev: CalEvent | null) => void;
@@ -1,8 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Dialog } from "@rightkit/react-shell/react";
3
- import { eventDay, hhmm } from "@rightkit/mail-ui/lib/calendarUtils";
4
- import { openExternal } from "@rightkit/mail-ui/lib/ipc";
3
+ import { eventDay, hhmm } from "../lib/calendarUtils.js";
4
+ import { useWorkspaceClient } from "../host.js";
5
5
  export function CalendarEventPopover({ openEvent, setOpenEvent, openEventForm, deleteEv, rsvp, }) {
6
+ const { openExternal } = useWorkspaceClient();
6
7
  return (_jsxs(Dialog, { title: openEvent.summary || "(no title)", width: 420, onClose: () => setOpenEvent(null), children: [_jsxs("p", { className: "event-when", children: [openEvent.all_day
7
8
  ? new Date(eventDay(openEvent)).toLocaleDateString([], { weekday: "long", month: "long", day: "numeric" }) + " · all day"
8
9
  : `${new Date(openEvent.start).toLocaleDateString([], { weekday: "long", month: "long", day: "numeric" })} · ${hhmm(openEvent.start)}${openEvent.end ? `–${hhmm(openEvent.end)}` : ""}`, openEvent.is_recurring ? " · repeats" : ""] }), openEvent.location && _jsx("p", { className: "hint", children: openEvent.location }), openEvent.description && _jsx("p", { className: "event-desc", children: openEvent.description }), openEvent.attendees.length > 0 && (_jsxs("p", { className: "hint", children: [openEvent.attendees.length, " guest", openEvent.attendees.length > 1 ? "s" : ""] })), _jsxs("div", { className: "ws-detail-actions", children: [openEvent.meet_link && (_jsx(Button, { variant: "primary", size: "sm", onClick: () => openExternal(openEvent.meet_link), children: "Join Meet" })), openEvent.my_response && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "ghost", size: "sm", onClick: () => rsvp(openEvent, "accepted"), children: "Yes" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => rsvp(openEvent, "tentative"), children: "Maybe" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => rsvp(openEvent, "declined"), children: "No" })] })), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => openEventForm(openEvent), children: "Edit" }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => deleteEv(openEvent), children: "Delete" })] })] }));
@@ -1,4 +1,4 @@
1
- import type { CalEvent } from "@rightkit/mail-ui/lib/ipc";
1
+ import type { CalEvent } from "../client.js";
2
2
  export declare function MonthGrid({ cursor, events, today, onDay, onEvent }: {
3
3
  cursor: number;
4
4
  events: CalEvent[];
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button } from "@rightkit/react-shell/react";
3
3
  import { useEffect, useMemo, useRef } from "react";
4
- import { DAY, WEEKDAY_LABELS, eventDay, hhmm, sameDay, startOfWeek } from "@rightkit/mail-ui/lib/calendarUtils";
4
+ import { DAY, WEEKDAY_LABELS, eventDay, hhmm, sameDay, startOfWeek } from "../lib/calendarUtils.js";
5
5
  export function MonthGrid({ cursor, events, today, onDay, onEvent }) {
6
6
  const d = new Date(cursor);
7
7
  const first = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
@@ -1,42 +1,41 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, IconButton, Dialog, SegmentedControl } from "@rightkit/react-shell/react";
3
3
  import { memo, useEffect, useRef, useState } from "react";
4
- import Icon from "@rightkit/mail-ui/components/Icons";
5
- import { AccountSwitcher } from "@rightkit/mail-ui/components/AccountSwitcher";
6
- import { setPref } from "@rightkit/mail-ui/lib/preferences";
7
- import { useAppStore } from "@rightkit/mail-ui/store/index";
8
- import { showDialog } from "@rightkit/mail-ui/hooks/dialog";
9
- import { listCalendars, listEvents, createEvent, updateEvent, deleteEvent, truncateEventSeries, rsvpEvent, freeBusy, } from "@rightkit/mail-ui/lib/ipc";
4
+ import { Icon } from "../icons.js";
10
5
  import { MonthGrid, TimeGrid } from "./CalendarGrids.js";
11
- import { DAY, rangeFor, startOfDay, startOfWeek } from "@rightkit/mail-ui/lib/calendarUtils";
6
+ import { DAY, rangeFor, startOfDay, startOfWeek } from "../lib/calendarUtils.js";
12
7
  import { CalendarEventFormModal } from "./CalendarEventFormModal.js";
13
8
  import { CalendarEventPopover } from "./CalendarEventPopover.js";
9
+ import { HostAccountSwitcher, useWorkspaceClient, useWorkspaceHost, useWorkspaceState } from "../host.js";
14
10
  const CALENDAR_VIEWS = ["month", "week", "day"];
15
11
  function CalendarViewInner() {
16
12
  const [recurScope, setRecurScope] = useState(null);
17
- const activeAccount = useAppStore((s) => s.activeAccount);
18
- const setErr = useAppStore((s) => s.setErr);
19
- const showUndo = useAppStore((s) => s.showUndo);
20
- const setCalendars = useAppStore((s) => s.setCalendars);
21
- const selectedCal = useAppStore((s) => s.selectedCal);
22
- const setSelectedCal = useAppStore((s) => s.setSelectedCal);
23
- const shownCals = useAppStore((s) => s.shownCals);
24
- const setShownCals = useAppStore((s) => s.setShownCals);
25
- const calView = useAppStore((s) => s.calView);
26
- const setCalView = useAppStore((s) => s.setCalView);
27
- const calCursor = useAppStore((s) => s.calCursor);
28
- const setCalCursor = useAppStore((s) => s.setCalCursor);
29
- const events = useAppStore((s) => s.events);
30
- const setEvents = useAppStore((s) => s.setEvents);
31
- const openEvent = useAppStore((s) => s.openEvent);
32
- const setOpenEvent = useAppStore((s) => s.setOpenEvent);
33
- const eventForm = useAppStore((s) => s.eventForm);
34
- const setEventForm = useAppStore((s) => s.setEventForm);
35
- const freeBusyMsg = useAppStore((s) => s.freeBusyMsg);
36
- const setFreeBusyMsg = useAppStore((s) => s.setFreeBusyMsg);
37
- const setCalendarBusy = useAppStore((s) => s.setCalendarBusy);
38
- const eventSeed = useAppStore((s) => s.eventSeed);
39
- const setEventSeed = useAppStore((s) => s.setEventSeed);
13
+ const { listCalendars, listEvents, createEvent, updateEvent, deleteEvent, truncateEventSeries, rsvpEvent, freeBusy } = useWorkspaceClient();
14
+ const showDialog = useWorkspaceHost().dialog;
15
+ const setPref = useWorkspaceHost().savePreference;
16
+ const activeAccount = useWorkspaceState((s) => s.activeAccount);
17
+ const setErr = useWorkspaceHost().reportError;
18
+ const showUndo = useWorkspaceHost().notify;
19
+ const setCalendars = useWorkspaceState((s) => s.setCalendars);
20
+ const selectedCal = useWorkspaceState((s) => s.selectedCal);
21
+ const setSelectedCal = useWorkspaceState((s) => s.setSelectedCal);
22
+ const shownCals = useWorkspaceState((s) => s.shownCals);
23
+ const setShownCals = useWorkspaceState((s) => s.setShownCals);
24
+ const calView = useWorkspaceState((s) => s.calView);
25
+ const setCalView = useWorkspaceState((s) => s.setCalView);
26
+ const calCursor = useWorkspaceState((s) => s.calCursor);
27
+ const setCalCursor = useWorkspaceState((s) => s.setCalCursor);
28
+ const events = useWorkspaceState((s) => s.events);
29
+ const setEvents = useWorkspaceState((s) => s.setEvents);
30
+ const openEvent = useWorkspaceState((s) => s.openEvent);
31
+ const setOpenEvent = useWorkspaceState((s) => s.setOpenEvent);
32
+ const eventForm = useWorkspaceState((s) => s.eventForm);
33
+ const setEventForm = useWorkspaceState((s) => s.setEventForm);
34
+ const freeBusyMsg = useWorkspaceState((s) => s.freeBusyMsg);
35
+ const setFreeBusyMsg = useWorkspaceState((s) => s.setFreeBusyMsg);
36
+ const setCalendarBusy = useWorkspaceState((s) => s.setCalendarBusy);
37
+ const eventSeed = useWorkspaceState((s) => s.eventSeed);
38
+ const setEventSeed = useWorkspaceState((s) => s.setEventSeed);
40
39
  const [calendarNow] = useState(() => {
41
40
  const now = new Date();
42
41
  return {
@@ -192,7 +191,7 @@ function CalendarViewInner() {
192
191
  await createEvent(activeAccount, selectedCal, ev);
193
192
  setEventForm(null);
194
193
  await reloadEvents();
195
- showUndo(f.id ? "Event updated" : "Event created", [], "");
194
+ showUndo(f.id ? "Event updated" : "Event created");
196
195
  }
197
196
  catch (e) {
198
197
  setErr(String(e));
@@ -207,7 +206,7 @@ function CalendarViewInner() {
207
206
  setOpenEvent(null);
208
207
  return reloadEvents();
209
208
  })
210
- .then(() => showUndo("Event deleted", [], ""))
209
+ .then(() => showUndo("Event deleted"))
211
210
  .catch((e) => setErr(String(e)));
212
211
  }
213
212
  function performTruncate(ev) {
@@ -219,7 +218,7 @@ function CalendarViewInner() {
219
218
  setOpenEvent(null);
220
219
  return reloadEvents();
221
220
  })
222
- .then(() => showUndo("This and following events removed", [], ""))
221
+ .then(() => showUndo("This and following events removed"))
223
222
  .catch((e) => setErr(String(e)));
224
223
  }
225
224
  async function deleteEv(ev) {
@@ -240,7 +239,7 @@ function CalendarViewInner() {
240
239
  .then(() => reloadEvents())
241
240
  .then(() => {
242
241
  setOpenEvent(null);
243
- showUndo(`RSVP: ${response}`, [], "");
242
+ showUndo(`RSVP: ${response}`);
244
243
  })
245
244
  .catch((e) => setErr(String(e)));
246
245
  }
@@ -326,7 +325,7 @@ function CalendarViewInner() {
326
325
  }
327
326
  return d.toLocaleDateString([], { month: "long", year: "numeric" });
328
327
  })();
329
- return (_jsxs("div", { className: "calview", children: [_jsxs("div", { className: "cal-bar", children: [_jsx(Button, { variant: "ghost", className: "cal-today", onClick: () => setCalCursor(Date.now()), children: "Today" }), _jsx(IconButton, { label: "Previous", className: "cal-step", title: "Previous", onClick: () => step(-1), children: _jsx(Icon, { name: "chevron-left", size: 18 }) }), _jsx(IconButton, { label: "Next", className: "cal-step", title: "Next", onClick: () => step(1), children: _jsx(Icon, { name: "chevron-right", size: 18 }) }), _jsx("h2", { className: "cal-label", children: headerLabel }), _jsx("div", { className: "cal-bar-spacer" }), _jsx("div", { className: "cal-viewtoggle", children: _jsx(SegmentedControl, { label: "Calendar view", value: calView, options: CALENDAR_VIEWS.map((v) => ({ value: v, label: v[0].toUpperCase() + v.slice(1) })), onChange: (v) => pickView(v) }) }), _jsx(AccountSwitcher, {})] }), _jsx("div", { className: "cal-grid", onWheel: onWheel, children: calView === "month" ? (_jsx(MonthGrid, { cursor: calCursor, events: events, today: calendarNow.today, onDay: (ms) => openEventForm(undefined, undefined, false, ms + 9 * 3600000), onEvent: setOpenEvent })) : (_jsx(TimeGrid, { days: calView === "week"
328
+ return (_jsxs("div", { className: "calview", children: [_jsxs("div", { className: "cal-bar", children: [_jsx(Button, { variant: "ghost", className: "cal-today", onClick: () => setCalCursor(Date.now()), children: "Today" }), _jsx(IconButton, { label: "Previous", className: "cal-step", title: "Previous", onClick: () => step(-1), children: _jsx(Icon, { name: "chevron-left", size: 18 }) }), _jsx(IconButton, { label: "Next", className: "cal-step", title: "Next", onClick: () => step(1), children: _jsx(Icon, { name: "chevron-right", size: 18 }) }), _jsx("h2", { className: "cal-label", children: headerLabel }), _jsx("div", { className: "cal-bar-spacer" }), _jsx("div", { className: "cal-viewtoggle", children: _jsx(SegmentedControl, { label: "Calendar view", value: calView, options: CALENDAR_VIEWS.map((v) => ({ value: v, label: v[0].toUpperCase() + v.slice(1) })), onChange: (v) => pickView(v) }) }), _jsx(HostAccountSwitcher, {})] }), _jsx("div", { className: "cal-grid", onWheel: onWheel, children: calView === "month" ? (_jsx(MonthGrid, { cursor: calCursor, events: events, today: calendarNow.today, onDay: (ms) => openEventForm(undefined, undefined, false, ms + 9 * 3600000), onEvent: setOpenEvent })) : (_jsx(TimeGrid, { days: calView === "week"
330
329
  ? Array.from({ length: 7 }, (_, i) => startOfWeek(calCursor) + i * DAY)
331
330
  : [startOfDay(calCursor)], events: events, today: calendarNow.today, nowMin: calendarNow.minute, onSlot: (ms) => openEventForm(undefined, undefined, false, ms), onEvent: setOpenEvent })) }), openEvent && (_jsx(CalendarEventPopover, { openEvent: openEvent, setOpenEvent: setOpenEvent, openEventForm: openEventForm, deleteEv: deleteEv, rsvp: rsvp })), eventForm && (_jsx(CalendarEventFormModal, { eventForm: eventForm, setEventForm: setEventForm, freeBusyMsg: freeBusyMsg, checkAvailability: checkAvailability, submitEvent: submitEvent })), recurScope && (_jsxs(Dialog, { title: recurScope.mode === "edit" ? "Edit recurring event" : "Delete recurring event", width: 420, onClose: () => setRecurScope(null), children: [_jsxs("p", { className: "hint", children: ["\u201C", recurScope.ev.summary, "\u201D repeats. Apply to:"] }), _jsxs("div", { className: "recur-choices", children: [_jsx(Button, { variant: "primary", onClick: () => recurScope.mode === "edit" ? fillEditForm(recurScope.ev, recurScope.ev.id) : performDelete(recurScope.ev, recurScope.ev.id), children: "This event" }), recurScope.mode === "delete" && (_jsx(Button, { variant: "primary", onClick: () => performTruncate(recurScope.ev), children: "This and following events" })), _jsx(Button, { variant: "primary", onClick: () => recurScope.mode === "edit" ? fillEditForm(recurScope.ev, recurScope.ev.recurring_event_id) : performDelete(recurScope.ev, recurScope.ev.recurring_event_id), children: "All events" })] })] }))] }));
332
331
  }
@@ -3,29 +3,27 @@ import { Button, Dialog, SkeletonRows, Input } from "@rightkit/react-shell/react
3
3
  // Contacts — a browsable list (parity with contacts.google.com): search +
4
4
  // full contact list with avatars, a create/edit modal, delete, and the
5
5
  // import/export/dedupe tools. Group filtering reads the active group from the
6
- // store (set in ContactsNav). Directory search falls back to the People API for
6
+ // host state (set in ContactsNav). Directory search falls back to the People API for
7
7
  // addresses outside the personal contact list.
8
8
  import { memo, useEffect, useMemo, useRef, useState } from "react";
9
- import Icon from "@rightkit/mail-ui/components/Icons";
10
- import { AccountSwitcher } from "@rightkit/mail-ui/components/AccountSwitcher";
11
- import { colorFor, initialOf } from "@rightkit/mail-ui/lib/mail";
9
+ import { Icon } from "../icons.js";
10
+ import { colorFor, initialOf } from "../lib/avatar.js";
12
11
  import { buildVcf, parseVcf, parseContactsCsv } from "../lib/vcard.js";
13
- import { useAppStore } from "@rightkit/mail-ui/store/index";
14
- import { showDialog } from "@rightkit/mail-ui/hooks/dialog";
15
- import { searchRecipients, listAllContacts, createContact, updateContact, deleteContact, saveTextDownload, } from "@rightkit/mail-ui/lib/ipc";
16
12
  import { EMPTY_EDITOR } from "./ContactsView.types.js";
13
+ import { HostAccountSwitcher, useWorkspaceClient, useWorkspaceHost, useWorkspaceState } from "../host.js";
17
14
  function ContactsViewInner() {
18
- const openComposeTo = useAppStore((s) => s.openComposeTo);
19
- const activeAccount = useAppStore((s) => s.activeAccount);
20
- const setErr = useAppStore((s) => s.setErr);
21
- const showUndo = useAppStore((s) => s.showUndo);
22
- const setView = useAppStore((s) => s.setView);
23
- const contactsQuery = useAppStore((s) => s.contactsQuery);
24
- const setContactsQuery = useAppStore((s) => s.setContactsQuery);
25
- const contactsBusy = useAppStore((s) => s.contactsBusy);
26
- const setContactsBusy = useAppStore((s) => s.setContactsBusy);
27
- const contactDupes = useAppStore((s) => s.contactDupes);
28
- const setContactDupes = useAppStore((s) => s.setContactDupes);
15
+ const { searchRecipients, listAllContacts, createContact, updateContact, deleteContact, saveTextDownload } = useWorkspaceClient();
16
+ const showDialog = useWorkspaceHost().dialog;
17
+ const openComposeTo = useWorkspaceHost().composeTo;
18
+ const activeAccount = useWorkspaceState((s) => s.activeAccount);
19
+ const setErr = useWorkspaceHost().reportError;
20
+ const showUndo = useWorkspaceHost().notify;
21
+ const contactsQuery = useWorkspaceState((s) => s.contactsQuery);
22
+ const setContactsQuery = useWorkspaceState((s) => s.setContactsQuery);
23
+ const contactsBusy = useWorkspaceState((s) => s.contactsBusy);
24
+ const setContactsBusy = useWorkspaceState((s) => s.setContactsBusy);
25
+ const contactDupes = useWorkspaceState((s) => s.contactDupes);
26
+ const setContactDupes = useWorkspaceState((s) => s.setContactDupes);
29
27
  const [contacts, setContacts] = useState([]);
30
28
  const [dirResults, setDirResults] = useState([]);
31
29
  const [editor, setEditor] = useState(null);
@@ -75,8 +73,7 @@ function ContactsViewInner() {
75
73
  return dirResults.filter((r) => r.email && !known.has(r.email.toLowerCase()));
76
74
  }, [dirResults, contacts, query]);
77
75
  function emailContact(addr) {
78
- setView("inbox");
79
- setTimeout(() => openComposeTo(addr, "", ""), 0);
76
+ openComposeTo(addr, "", "");
80
77
  }
81
78
  function startCreate() {
82
79
  setEditor(EMPTY_EDITOR);
@@ -105,7 +102,7 @@ function ContactsViewInner() {
105
102
  ? updateContact(activeAccount, editor.resource_name, editor.etag, name, emails, phones)
106
103
  : createContact(activeAccount, name, emails, phones);
107
104
  op.then(() => {
108
- showUndo(editor.resource_name ? "Contact saved" : "Contact created", [], "");
105
+ showUndo(editor.resource_name ? "Contact saved" : "Contact created");
109
106
  setEditor(null);
110
107
  loadAll();
111
108
  })
@@ -120,7 +117,7 @@ function ContactsViewInner() {
120
117
  setContactsBusy(true);
121
118
  deleteContact(activeAccount, c.resource_name)
122
119
  .then(() => {
123
- showUndo("Contact deleted", [], "");
120
+ showUndo("Contact deleted");
124
121
  setContacts((prev) => prev.filter((x) => x.resource_name !== c.resource_name));
125
122
  })
126
123
  .catch((e) => setErr(String(e)))
@@ -132,7 +129,7 @@ function ContactsViewInner() {
132
129
  const vcf = buildVcf(contacts.map((c) => ({ name: c.display_name, emails: c.emails, phones: c.phones, org: c.org })));
133
130
  // Saved natively to Downloads (exclusive atomic publish), like every other download.
134
131
  saveTextDownload("contacts.vcf", vcf)
135
- .then(() => showUndo(`Exported ${contacts.length} contacts`, [], ""))
132
+ .then(() => showUndo(`Exported ${contacts.length} contacts`))
136
133
  .catch((e) => setErr(String(e)));
137
134
  }
138
135
  function importContactsFile(file) {
@@ -162,7 +159,7 @@ function ContactsViewInner() {
162
159
  break;
163
160
  }
164
161
  }
165
- showUndo(`Imported ${ok} of ${parsed.length} contacts`, [], "");
162
+ showUndo(`Imported ${ok} of ${parsed.length} contacts`);
166
163
  loadAll();
167
164
  })
168
165
  .catch((e) => setErr(String(e)))
@@ -196,7 +193,7 @@ function ContactsViewInner() {
196
193
  }
197
194
  setContactDupes(groups);
198
195
  if (groups.length === 0)
199
- showUndo("No duplicate contacts found", [], "");
196
+ showUndo("No duplicate contacts found");
200
197
  }
201
198
  async function mergeDupeGroup(group) {
202
199
  if (!activeAccount || contactsBusy)
@@ -212,7 +209,7 @@ function ContactsViewInner() {
212
209
  .then(async () => {
213
210
  for (const d of group.dups)
214
211
  await deleteContact(activeAccount, d.resource_name);
215
- showUndo("Merged duplicates", [], "");
212
+ showUndo("Merged duplicates");
216
213
  setContactDupes((prev) => (prev ? prev.filter((g) => g !== group) : prev));
217
214
  loadAll();
218
215
  })
@@ -220,7 +217,7 @@ function ContactsViewInner() {
220
217
  .finally(() => setContactsBusy(false));
221
218
  }
222
219
  const setEd = (patch) => setEditor((e) => (e ? { ...e, ...patch } : e));
223
- return (_jsxs("div", { className: "contactsview", children: [_jsxs("div", { className: "contacts-bar", children: [_jsx("h2", { className: "workspace-title", children: "Contacts" }), _jsx("span", { className: "contacts-count", children: contacts.length }), _jsx("div", { className: "drive-bar-spacer" }), _jsx(Input, { className: "workspace-search", placeholder: "Search contacts & directory\u2026", value: contactsQuery, onChange: (e) => onSearch(e.target.value) }), _jsx(Button, { variant: "ghost", size: "sm", disabled: contactsBusy, onClick: exportContacts, children: "Export" }), _jsx(Button, { variant: "ghost", size: "sm", disabled: contactsBusy, onClick: () => fileRef.current?.click(), children: "Import" }), _jsx(Button, { variant: "ghost", size: "sm", disabled: contactsBusy, onClick: findContactDupes, children: "Find duplicates" }), _jsxs(Button, { variant: "primary", size: "sm", onClick: startCreate, children: [_jsx(Icon, { name: "person-add", size: 15 }), " Create"] }), _jsx(AccountSwitcher, {}), _jsx(Input, { ref: fileRef, type: "file", accept: ".vcf,.csv", style: { display: "none" }, onChange: (e) => {
220
+ return (_jsxs("div", { className: "contactsview", children: [_jsxs("div", { className: "contacts-bar", children: [_jsx("h2", { className: "workspace-title", children: "Contacts" }), _jsx("span", { className: "contacts-count", children: contacts.length }), _jsx("div", { className: "drive-bar-spacer" }), _jsx(Input, { className: "workspace-search", placeholder: "Search contacts & directory\u2026", value: contactsQuery, onChange: (e) => onSearch(e.target.value) }), _jsx(Button, { variant: "ghost", size: "sm", disabled: contactsBusy, onClick: exportContacts, children: "Export" }), _jsx(Button, { variant: "ghost", size: "sm", disabled: contactsBusy, onClick: () => fileRef.current?.click(), children: "Import" }), _jsx(Button, { variant: "ghost", size: "sm", disabled: contactsBusy, onClick: findContactDupes, children: "Find duplicates" }), _jsxs(Button, { variant: "primary", size: "sm", onClick: startCreate, children: [_jsx(Icon, { name: "person-add", size: 15 }), " Create"] }), _jsx(HostAccountSwitcher, {}), _jsx(Input, { ref: fileRef, type: "file", accept: ".vcf,.csv", style: { display: "none" }, onChange: (e) => {
224
221
  const f = e.target.files?.[0];
225
222
  if (f)
226
223
  importContactsFile(f);
@@ -1,5 +1,5 @@
1
- import { useDrive } from "@rightkit/mail-ui/hooks/useDrive";
2
- import { type DriveFile } from "@rightkit/mail-ui/lib/ipc";
1
+ import { useDrive } from "../hooks/useDrive.js";
2
+ import type { DriveFile } from "../client.js";
3
3
  type DriveFileMenuProps = {
4
4
  f: DriveFile;
5
5
  anchor: Element;
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { ContextMenu } from "@rightkit/react-shell/react";
4
- import { openExternal } from "@rightkit/mail-ui/lib/ipc";
5
- import { useAppStore } from "@rightkit/mail-ui/store/index";
4
+ import { useWorkspaceClient, useWorkspaceHost } from "../host.js";
6
5
  /** The per-file dropdown (preview/open/download/attach/rename/move/share/trash). */
7
6
  export function DriveFileMenu({ f, anchor, onClose, onPreview, drive, onMove, onShare }) {
8
- const setErr = useAppStore((s) => s.setErr);
7
+ const { openExternal } = useWorkspaceClient();
8
+ const setErr = useWorkspaceHost().reportError;
9
9
  const items = useMemo(() => {
10
10
  const list = [];
11
11
  if (!f.is_folder)
@@ -1,5 +1,5 @@
1
- import { useDrive } from "@rightkit/mail-ui/hooks/useDrive";
2
- import { type DriveFile } from "@rightkit/mail-ui/lib/ipc";
1
+ import { useDrive } from "../hooks/useDrive.js";
2
+ import type { DriveFile } from "../client.js";
3
3
  type DrivePreviewModalProps = {
4
4
  drivePreview: DriveFile;
5
5
  setDrivePreview: (f: DriveFile | null) => void;
@@ -1,11 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Dialog } from "@rightkit/react-shell/react";
3
- import Icon from "@rightkit/mail-ui/components/Icons";
4
- import { openExternal } from "@rightkit/mail-ui/lib/ipc";
3
+ import { Icon } from "../icons.js";
5
4
  import { formatBytes } from "@rightkit/settings-ui/core";
6
- import { useAppStore } from "@rightkit/mail-ui/store/index";
7
5
  import { fileGlyph } from "./driveFileGlyph.js";
6
+ import { useWorkspaceClient, useWorkspaceHost } from "../host.js";
8
7
  export function DrivePreviewModal({ drivePreview, setDrivePreview, drive }) {
9
- const setErr = useAppStore((s) => s.setErr);
8
+ const { openExternal } = useWorkspaceClient();
9
+ const setErr = useWorkspaceHost().reportError;
10
10
  return (_jsxs(Dialog, { title: drivePreview.name, width: 520, onClose: () => setDrivePreview(null), children: [drivePreview.thumbnail_link ? (_jsx("img", { className: "dp-thumb", src: drivePreview.thumbnail_link, alt: "", onError: (e) => { e.currentTarget.style.display = "none"; } })) : (_jsxs("div", { className: "dp-noimg", children: [_jsx(Icon, { name: fileGlyph(drivePreview).icon, size: 48 }), _jsx("span", { className: "hint", children: "No preview available" })] })), _jsxs("div", { className: "dp-meta hint", children: [drivePreview.mime_type, drivePreview.size ? ` · ${formatBytes(Number(drivePreview.size))}` : "", drivePreview.modified_time ? ` · ${new Date(drivePreview.modified_time).toLocaleString()}` : ""] }), _jsxs("div", { className: "dp-actions", children: [drivePreview.web_view_link && (_jsx(Button, { variant: "primary", onClick: () => openExternal(drivePreview.web_view_link).catch((e) => setErr(String(e))), children: "Open in Drive" })), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => drive.download(drivePreview), children: "Download" }), _jsx(Button, { variant: "ghost", size: "sm", onClick: () => { drive.insertDriveLink(drivePreview); setDrivePreview(null); }, children: "Insert link in email" })] })] }));
11
11
  }
@@ -4,16 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // cards with thumbnails or colored type tiles, each with a hover ⋮ menu.
5
5
  // Shelf nav + storage live in DriveNav; shared Drive logic in useDrive.
6
6
  import { memo, useEffect, useState } from "react";
7
- import Icon from "@rightkit/mail-ui/components/Icons";
8
- import { AccountSwitcher } from "@rightkit/mail-ui/components/AccountSwitcher";
7
+ import { Icon } from "../icons.js";
9
8
  import { Button, Dialog, IconButton, Skeleton, SegmentedControl, Input, Select } from "@rightkit/react-shell/react";
10
- import { setPref } from "@rightkit/mail-ui/lib/preferences";
11
- import { useAppStore } from "@rightkit/mail-ui/store/index";
12
- import { useDrive, FOLDER_MIME } from "@rightkit/mail-ui/hooks/useDrive";
9
+ import { useDrive, FOLDER_MIME } from "../hooks/useDrive.js";
13
10
  import { DriveFileMenu } from "./DriveFileMenu.js";
14
11
  import { DrivePreviewModal } from "./DrivePreviewModal.js";
15
12
  import { fileGlyph } from "./driveFileGlyph.js";
16
- import { listDriveFiles, driveStorageQuota, driveMove, driveShare, driveListPermissions, driveRevokePermission, } from "@rightkit/mail-ui/lib/ipc";
13
+ import { HostAccountSwitcher, useWorkspaceClient, useWorkspaceHost, useWorkspaceState } from "../host.js";
17
14
  /** Native button that carries the keyboard/AT semantics of a file card or row;
18
15
  * the mouse click is handled by the card itself. */
19
16
  const FILE_OPEN_STYLE = {
@@ -38,20 +35,22 @@ const SHELF_LABEL = {
38
35
  };
39
36
  function DriveViewInner() {
40
37
  const drive = useDrive();
41
- const setErr = useAppStore((s) => s.setErr);
42
- const showUndo = useAppStore((s) => s.showUndo);
43
- const setDrivePath = useAppStore((s) => s.setDrivePath);
44
- const setDriveQuery = useAppStore((s) => s.setDriveQuery);
45
- const driveFiles = useAppStore((s) => s.driveFiles);
46
- const setDriveFiles = useAppStore((s) => s.setDriveFiles);
47
- const driveToken = useAppStore((s) => s.driveToken);
48
- const setDriveToken = useAppStore((s) => s.setDriveToken);
49
- const drivePreview = useAppStore((s) => s.drivePreview);
50
- const setDrivePreview = useAppStore((s) => s.setDrivePreview);
51
- const driveViewMode = useAppStore((s) => s.driveViewMode);
52
- const setDriveViewMode = useAppStore((s) => s.setDriveViewMode);
53
- const setDriveQuota = useAppStore((s) => s.setDriveQuota);
54
- const driveBusy = useAppStore((s) => s.driveBusy);
38
+ const { listDriveFiles, driveStorageQuota, driveMove, driveShare, driveListPermissions, driveRevokePermission } = useWorkspaceClient();
39
+ const setPref = useWorkspaceHost().savePreference;
40
+ const setErr = useWorkspaceHost().reportError;
41
+ const showUndo = useWorkspaceHost().notify;
42
+ const setDrivePath = useWorkspaceState((s) => s.setDrivePath);
43
+ const setDriveQuery = useWorkspaceState((s) => s.setDriveQuery);
44
+ const driveFiles = useWorkspaceState((s) => s.driveFiles);
45
+ const setDriveFiles = useWorkspaceState((s) => s.setDriveFiles);
46
+ const driveToken = useWorkspaceState((s) => s.driveToken);
47
+ const setDriveToken = useWorkspaceState((s) => s.setDriveToken);
48
+ const drivePreview = useWorkspaceState((s) => s.drivePreview);
49
+ const setDrivePreview = useWorkspaceState((s) => s.setDrivePreview);
50
+ const driveViewMode = useWorkspaceState((s) => s.driveViewMode);
51
+ const setDriveViewMode = useWorkspaceState((s) => s.setDriveViewMode);
52
+ const setDriveQuota = useWorkspaceState((s) => s.setDriveQuota);
53
+ const driveBusy = useWorkspaceState((s) => s.driveBusy);
55
54
  const { activeAccount, driveShelf, drivePath, driveQuery, driveType } = drive;
56
55
  // Transient UI state: the open ⋮ menu, the move/share modals, drag-over state.
57
56
  const [menuFor, setMenuFor] = useState(null);
@@ -89,7 +88,7 @@ function DriveViewInner() {
89
88
  const oldParent = driveShelf === "my" ? drive.currentFolderId() ?? "root" : undefined;
90
89
  driveMove(activeAccount, moveFile.id, targetId, oldParent)
91
90
  .then(() => {
92
- showUndo(`Moved to ${targetName}`, [], "");
91
+ showUndo(`Moved to ${targetName}`);
93
92
  setMoveFile(null);
94
93
  drive.refresh();
95
94
  })
@@ -110,7 +109,7 @@ function DriveViewInner() {
110
109
  return;
111
110
  driveShare(activeAccount, shareFile.id, shareEmail.trim(), shareRole)
112
111
  .then(() => {
113
- showUndo(`Shared with ${shareEmail.trim()} (${shareRole})`, [], "");
112
+ showUndo(`Shared with ${shareEmail.trim()} (${shareRole})`);
114
113
  setShareEmail("");
115
114
  driveListPermissions(activeAccount, shareFile.id).then(setPerms).catch(() => { });
116
115
  })
@@ -122,7 +121,7 @@ function DriveViewInner() {
122
121
  driveRevokePermission(activeAccount, shareFile.id, permId)
123
122
  .then(() => {
124
123
  setPerms((prev) => prev.filter((p) => p.id !== permId));
125
- showUndo("Access removed", [], "");
124
+ showUndo("Access removed");
126
125
  })
127
126
  .catch((e) => setErr(String(e)));
128
127
  }
@@ -148,7 +147,7 @@ function DriveViewInner() {
148
147
  ], onChange: drive.setTypeFilter }), _jsx("div", { className: "drive-toggle", children: _jsx(SegmentedControl, { label: "Drive view", value: driveViewMode, options: [
149
148
  { value: "list", label: _jsx(Icon, { name: "list", size: 18 }), title: "List view" },
150
149
  { value: "grid", label: _jsx(Icon, { name: "grid", size: 18 }), title: "Grid view" },
151
- ], onChange: (m) => { setDriveViewMode(m); setPref("driveViewMode", m); } }) }), _jsx(AccountSwitcher, {})] }), _jsxs("div", { className: `drive-scroll ${dragOver ? "drag-over" : ""}`, onDragOver: (e) => { e.preventDefault(); if (!dragOver)
150
+ ], onChange: (m) => { setDriveViewMode(m); setPref("driveViewMode", m); } }) }), _jsx(HostAccountSwitcher, {})] }), _jsxs("div", { className: `drive-scroll ${dragOver ? "drag-over" : ""}`, onDragOver: (e) => { e.preventDefault(); if (!dragOver)
152
151
  setDragOver(true); }, onDragLeave: (e) => { if (e.currentTarget === e.target)
153
152
  setDragOver(false); }, onDrop: (e) => {
154
153
  e.preventDefault();