@remit/ui 0.0.113 → 0.0.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/app-shell-slotted.render.test.ts +83 -0
- package/src/components/app-shell-slotted.tsx +31 -3
- package/src/components/attendee-row.render.test.ts +73 -0
- package/src/components/attendee-row.stories.tsx +68 -0
- package/src/components/attendee-row.tsx +96 -0
- package/src/components/calendar-event-chip.render.test.ts +67 -0
- package/src/components/calendar-event-chip.stories.tsx +128 -0
- package/src/components/calendar-event-chip.tsx +116 -0
- package/src/components/calendar-list.render.test.ts +77 -0
- package/src/components/calendar-list.stories.tsx +130 -0
- package/src/components/calendar-list.tsx +225 -0
- package/src/components/calendar-toolbar.render.test.ts +69 -0
- package/src/components/calendar-toolbar.stories.tsx +55 -0
- package/src/components/calendar-toolbar.tsx +178 -0
- package/src/components/calendar-types.ts +135 -0
- package/src/components/custom-recurrence.stories.tsx +106 -0
- package/src/components/custom-recurrence.tsx +411 -0
- package/src/components/event-detail.render.test.ts +104 -0
- package/src/components/event-detail.stories.tsx +178 -0
- package/src/components/event-detail.tsx +188 -0
- package/src/components/event-editor-pane.tsx +54 -0
- package/src/components/event-editor.render.test.ts +83 -0
- package/src/components/event-editor.stories.tsx +99 -0
- package/src/components/event-editor.tsx +480 -0
- package/src/components/event-quick-entry.render.test.ts +40 -0
- package/src/components/event-quick-entry.stories.tsx +56 -0
- package/src/components/event-quick-entry.tsx +159 -0
- package/src/components/event-suggestion-card.render.test.ts +65 -0
- package/src/components/event-suggestion-card.stories.tsx +93 -0
- package/src/components/event-suggestion-card.tsx +116 -0
- package/src/components/flow-screen.tsx +169 -0
- package/src/components/intelligence-panel.stories.tsx +5 -1
- package/src/components/intelligence-panel.tsx +4 -0
- package/src/components/isolated-email-frame.tsx +1 -18
- package/src/components/message-list-pane.render.test.ts +2 -2
- package/src/components/message-list-pane.stories.tsx +1 -1
- package/src/components/nav-sidebar.tsx +20 -0
- package/src/components/popover-menu.tsx +230 -27
- package/src/components/pull-to-refresh.tsx +1 -19
- package/src/components/recurrence-scope-prompt.render.test.ts +36 -0
- package/src/components/recurrence-scope-prompt.stories.tsx +46 -0
- package/src/components/recurrence-scope-prompt.tsx +84 -0
- package/src/components/rich-text-correction-menu.tsx +220 -0
- package/src/components/rich-text-editor.stories.tsx +507 -5
- package/src/components/rich-text-editor.tsx +482 -83
- package/src/components/rich-text-spellcheck-menu.test.ts +865 -0
- package/src/components/rich-text-spellcheck-provider.test.ts +114 -1
- package/src/components/rich-text-spellcheck-provider.ts +68 -3
- package/src/components/rich-text-spellcheck-words.ts +168 -4
- package/src/components/rich-text-spellcheck-worker.ts +11 -0
- package/src/components/rich-text-spellcheck.test.ts +7 -0
- package/src/components/rich-text-spellcheck.ts +28 -2
- package/src/components/selection-wizard.tsx +19 -69
- package/src/index.ts +116 -0
- package/src/lib/calendar-color.ts +71 -0
- package/src/lib/event-phrase.test.ts +89 -0
- package/src/lib/event-phrase.ts +228 -0
- package/src/lib/recurrence.test.ts +141 -0
- package/src/lib/recurrence.ts +266 -0
- package/src/lib/use-match-media.ts +25 -0
- package/src/rich-text.ts +12 -0
- package/src/tokens.css +63 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
2
|
+
import { type ReactNode, useId } from "react";
|
|
3
|
+
import { cn } from "../lib/cn.js";
|
|
4
|
+
import type { CalendarViewId } from "./calendar-types.js";
|
|
5
|
+
|
|
6
|
+
export interface SegmentOption<T extends string> {
|
|
7
|
+
value: T;
|
|
8
|
+
label: string;
|
|
9
|
+
/** Shown instead of `label` where the control has to fit a thumb. */
|
|
10
|
+
shortLabel: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A flat choice, drawn the way the nav sidebar draws a picked mailbox: weight
|
|
15
|
+
* and hue mark what is on, and nothing is raised. The calendar's toolbar and the
|
|
16
|
+
* nav are the same act — choosing what the pane shows — so they look the same.
|
|
17
|
+
*/
|
|
18
|
+
export function segmentClassName(active: boolean): string {
|
|
19
|
+
return cn(
|
|
20
|
+
"flex cursor-pointer items-center justify-center rounded-md px-2 font-medium transition-colors",
|
|
21
|
+
"has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring",
|
|
22
|
+
active
|
|
23
|
+
? "bg-accent-2-soft text-accent-2"
|
|
24
|
+
: "text-fg-muted hover:bg-surface-sunken hover:text-fg",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SegmentedProps<T extends string> {
|
|
29
|
+
options: SegmentOption<T>[];
|
|
30
|
+
value: T;
|
|
31
|
+
onChange: (value: T) => void;
|
|
32
|
+
/** Grows every segment to a 44px touch target. */
|
|
33
|
+
touch?: boolean;
|
|
34
|
+
ariaLabel: string;
|
|
35
|
+
className?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function Segmented<T extends string>({
|
|
39
|
+
options,
|
|
40
|
+
value,
|
|
41
|
+
onChange,
|
|
42
|
+
touch,
|
|
43
|
+
ariaLabel,
|
|
44
|
+
className,
|
|
45
|
+
}: SegmentedProps<T>) {
|
|
46
|
+
const group = useId();
|
|
47
|
+
return (
|
|
48
|
+
<fieldset className={cn("inline-flex items-center gap-0.5", className)}>
|
|
49
|
+
<legend className="sr-only">{ariaLabel}</legend>
|
|
50
|
+
{options.map((option) => (
|
|
51
|
+
<label
|
|
52
|
+
key={option.value}
|
|
53
|
+
className={cn(
|
|
54
|
+
segmentClassName(value === option.value),
|
|
55
|
+
touch ? "min-h-11 flex-1 text-sm" : "h-7 text-xs",
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
<input
|
|
59
|
+
type="radio"
|
|
60
|
+
name={group}
|
|
61
|
+
value={option.value}
|
|
62
|
+
checked={value === option.value}
|
|
63
|
+
onChange={() => onChange(option.value)}
|
|
64
|
+
className="sr-only"
|
|
65
|
+
/>
|
|
66
|
+
{touch ? option.shortLabel : option.label}
|
|
67
|
+
</label>
|
|
68
|
+
))}
|
|
69
|
+
</fieldset>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const VIEW_OPTIONS: SegmentOption<CalendarViewId>[] = [
|
|
74
|
+
{ value: "year", label: "Year", shortLabel: "Y" },
|
|
75
|
+
{ value: "month", label: "Month", shortLabel: "M" },
|
|
76
|
+
{ value: "week", label: "Week", shortLabel: "W" },
|
|
77
|
+
{ value: "day", label: "Day", shortLabel: "D" },
|
|
78
|
+
{ value: "agenda", label: "Agenda", shortLabel: "List" },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
export interface CalendarViewSwitchProps {
|
|
82
|
+
value: CalendarViewId;
|
|
83
|
+
onChange: (view: CalendarViewId) => void;
|
|
84
|
+
/** Restricts the ladder — a phone has no room for a year grid. */
|
|
85
|
+
views?: CalendarViewId[];
|
|
86
|
+
touch?: boolean;
|
|
87
|
+
className?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The zoom ladder. Each step keeps the date in view, so moving between them is
|
|
92
|
+
* changing magnification rather than opening a different screen.
|
|
93
|
+
*/
|
|
94
|
+
export function CalendarViewSwitch({
|
|
95
|
+
value,
|
|
96
|
+
onChange,
|
|
97
|
+
views,
|
|
98
|
+
touch,
|
|
99
|
+
className,
|
|
100
|
+
}: CalendarViewSwitchProps) {
|
|
101
|
+
const options = views
|
|
102
|
+
? VIEW_OPTIONS.filter((option) => views.includes(option.value))
|
|
103
|
+
: VIEW_OPTIONS;
|
|
104
|
+
return (
|
|
105
|
+
<Segmented
|
|
106
|
+
ariaLabel="Calendar view"
|
|
107
|
+
options={options}
|
|
108
|
+
value={value}
|
|
109
|
+
onChange={onChange}
|
|
110
|
+
touch={touch}
|
|
111
|
+
className={className}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface CalendarDateNavProps {
|
|
117
|
+
/** The range on screen, already formatted. */
|
|
118
|
+
title: string;
|
|
119
|
+
onPrev: () => void;
|
|
120
|
+
onNext: () => void;
|
|
121
|
+
onToday: () => void;
|
|
122
|
+
/** Right-hand controls: the view switch, whatever else the view owns. */
|
|
123
|
+
children?: ReactNode;
|
|
124
|
+
touch?: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Back, forward, and Today. Today lands on the same target from every view and
|
|
129
|
+
* every distance away, so it is a way home rather than a jump whose result
|
|
130
|
+
* depends on where you were.
|
|
131
|
+
*/
|
|
132
|
+
export function CalendarDateNav({
|
|
133
|
+
title,
|
|
134
|
+
onPrev,
|
|
135
|
+
onNext,
|
|
136
|
+
onToday,
|
|
137
|
+
children,
|
|
138
|
+
touch,
|
|
139
|
+
}: CalendarDateNavProps) {
|
|
140
|
+
const stepClass = cn(
|
|
141
|
+
"flex items-center justify-center rounded-md text-fg-muted outline-none transition-colors hover:bg-surface-sunken hover:text-fg focus-visible:ring-2 focus-visible:ring-ring",
|
|
142
|
+
touch ? "size-11" : "size-7",
|
|
143
|
+
);
|
|
144
|
+
return (
|
|
145
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={onPrev}
|
|
149
|
+
aria-label="Previous"
|
|
150
|
+
className={stepClass}
|
|
151
|
+
>
|
|
152
|
+
<ChevronLeft className="size-4" />
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
onClick={onNext}
|
|
157
|
+
aria-label="Next"
|
|
158
|
+
className={stepClass}
|
|
159
|
+
>
|
|
160
|
+
<ChevronRight className="size-4" />
|
|
161
|
+
</button>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={onToday}
|
|
165
|
+
className={cn(
|
|
166
|
+
"rounded-md border border-line px-2.5 font-medium text-fg outline-none transition-colors hover:bg-surface-sunken focus-visible:ring-2 focus-visible:ring-ring",
|
|
167
|
+
touch ? "min-h-11 text-sm" : "h-7 text-xs",
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
Today
|
|
171
|
+
</button>
|
|
172
|
+
<h2 className="min-w-0 flex-1 truncate text-sm font-semibold text-fg">
|
|
173
|
+
{title}
|
|
174
|
+
</h2>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shapes the calendar surfaces pass around. Presentational only: a
|
|
3
|
+
* component here takes one of these and renders it, and every mutation leaves
|
|
4
|
+
* through a callback. Nothing in this file knows where the data came from.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** One of the six per-calendar hues declared in `tokens.css`. */
|
|
8
|
+
export type CalendarColorId =
|
|
9
|
+
| "cal-1"
|
|
10
|
+
| "cal-2"
|
|
11
|
+
| "cal-3"
|
|
12
|
+
| "cal-4"
|
|
13
|
+
| "cal-5"
|
|
14
|
+
| "cal-6";
|
|
15
|
+
|
|
16
|
+
export const calendarColorIds: CalendarColorId[] = [
|
|
17
|
+
"cal-1",
|
|
18
|
+
"cal-2",
|
|
19
|
+
"cal-3",
|
|
20
|
+
"cal-4",
|
|
21
|
+
"cal-5",
|
|
22
|
+
"cal-6",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export type RsvpState = "accepted" | "tentative" | "declined" | "noReply";
|
|
26
|
+
|
|
27
|
+
export interface CalendarAttendee {
|
|
28
|
+
name: string;
|
|
29
|
+
email: string;
|
|
30
|
+
rsvp: RsvpState;
|
|
31
|
+
/** The one attendee who owns the invitation, if any is known. */
|
|
32
|
+
role: "organizer" | "attendee";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CalendarDescriptor {
|
|
36
|
+
id: string;
|
|
37
|
+
/** The mail account this calendar belongs to, so both nav trees line up. */
|
|
38
|
+
accountId: string;
|
|
39
|
+
accountLabel: string;
|
|
40
|
+
name: string;
|
|
41
|
+
color: CalendarColorId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* How much the source told us about the zone. `ambiguous` is a first-class
|
|
46
|
+
* state: a mail that said "3pm" and nothing else has no zone, and the UI says
|
|
47
|
+
* so rather than assuming the reader's.
|
|
48
|
+
*/
|
|
49
|
+
export type ZoneCertainty = "local" | "explicit" | "ambiguous";
|
|
50
|
+
|
|
51
|
+
export interface CalendarEventData {
|
|
52
|
+
id: string;
|
|
53
|
+
calendarId: string;
|
|
54
|
+
title: string;
|
|
55
|
+
/** ISO 8601 with an explicit offset. */
|
|
56
|
+
start: string;
|
|
57
|
+
end: string;
|
|
58
|
+
allDay: boolean;
|
|
59
|
+
location: string;
|
|
60
|
+
notes: string;
|
|
61
|
+
attendees: CalendarAttendee[];
|
|
62
|
+
/**
|
|
63
|
+
* The reader's own reply. Kept apart from `attendees`: how an event is drawn
|
|
64
|
+
* turns on what you said, not on what somebody else said.
|
|
65
|
+
*/
|
|
66
|
+
myRsvp: RsvpState;
|
|
67
|
+
/** The thread this event came from; empty when it was typed by hand. */
|
|
68
|
+
threadId: string;
|
|
69
|
+
threadSubject: string;
|
|
70
|
+
/** IANA zone, or empty when the source never said. */
|
|
71
|
+
timeZone: string;
|
|
72
|
+
zoneCertainty: ZoneCertainty;
|
|
73
|
+
/** The rule in words — "Every weekday, 09:15". Empty when it does not repeat. */
|
|
74
|
+
recurrenceRule: string;
|
|
75
|
+
/** Groups every instance of one series under a single object. */
|
|
76
|
+
seriesId: string;
|
|
77
|
+
/**
|
|
78
|
+
* This instance was moved or rewritten away from the rule and no longer
|
|
79
|
+
* matches it. The series still owns it, so the next edit still asks scope.
|
|
80
|
+
*/
|
|
81
|
+
seriesException: boolean;
|
|
82
|
+
status: "confirmed" | "tentative";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A candidate read out of mail. It is deliberately not a `CalendarEventData`:
|
|
87
|
+
* nothing reaches the grid until a person confirms it, so a suggestion cannot
|
|
88
|
+
* be mistaken for an event by type, let alone by pixel.
|
|
89
|
+
*/
|
|
90
|
+
export interface EventSuggestion {
|
|
91
|
+
id: string;
|
|
92
|
+
title: string;
|
|
93
|
+
start: string;
|
|
94
|
+
end: string;
|
|
95
|
+
allDay: boolean;
|
|
96
|
+
location: string;
|
|
97
|
+
/** The mail it was read out of. Always present — there is no other source. */
|
|
98
|
+
threadId: string;
|
|
99
|
+
threadSubject: string;
|
|
100
|
+
sender: string;
|
|
101
|
+
/** 0–1. The component derives its own wording; the seam carries the value. */
|
|
102
|
+
confidence: number;
|
|
103
|
+
/** What the parse could not settle, empty when nothing. */
|
|
104
|
+
ambiguity: string;
|
|
105
|
+
suggestedCalendarId: string;
|
|
106
|
+
timeZone: string;
|
|
107
|
+
zoneCertainty: ZoneCertainty;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Which instances of a series an edit applies to, chosen before the edit. */
|
|
111
|
+
export type RecurrenceScope = "this" | "following" | "all";
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The zoom levels of one continuous strip of time, widest first. They are not
|
|
115
|
+
* separate screens: changing one keeps the date you were looking at.
|
|
116
|
+
*/
|
|
117
|
+
export type CalendarViewId = "year" | "month" | "week" | "day" | "agenda";
|
|
118
|
+
|
|
119
|
+
/** What the editor holds while an event is being written. */
|
|
120
|
+
export interface EventDraft {
|
|
121
|
+
title: string;
|
|
122
|
+
/** `YYYY-MM-DD`. */
|
|
123
|
+
date: string;
|
|
124
|
+
/** `HH:MM`, empty when the entry is all day. */
|
|
125
|
+
startTime: string;
|
|
126
|
+
endTime: string;
|
|
127
|
+
allDay: boolean;
|
|
128
|
+
calendarId: string;
|
|
129
|
+
location: string;
|
|
130
|
+
/** Free text, one guest per comma — the prototype does not resolve them. */
|
|
131
|
+
guests: string;
|
|
132
|
+
notes: string;
|
|
133
|
+
/** Human-readable rule, empty for a one-off. */
|
|
134
|
+
repeat: string;
|
|
135
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
type CustomRecurrence,
|
|
5
|
+
defaultCustomRecurrence,
|
|
6
|
+
defaultEndDate,
|
|
7
|
+
formatCustomRecurrence,
|
|
8
|
+
} from "../lib/recurrence.js";
|
|
9
|
+
import { CustomRecurrenceEditor } from "./custom-recurrence.js";
|
|
10
|
+
|
|
11
|
+
const DATE = "2026-06-09";
|
|
12
|
+
const START = "09:15";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The rule the derived choices cannot express. Every part of it is a control —
|
|
16
|
+
* how often, which days, when it stops — and what comes out is the same
|
|
17
|
+
* sentence the picker reads back. Nobody types an RRULE and nobody is shown
|
|
18
|
+
* one; the line under each story is what the event would carry.
|
|
19
|
+
*/
|
|
20
|
+
const meta: Meta<typeof CustomRecurrenceEditor> = {
|
|
21
|
+
title: "Calendar/Custom recurrence",
|
|
22
|
+
component: CustomRecurrenceEditor,
|
|
23
|
+
parameters: { layout: "padded" },
|
|
24
|
+
};
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
type Story = StoryObj<typeof CustomRecurrenceEditor>;
|
|
28
|
+
|
|
29
|
+
function Editor({ seed, touch }: { seed: CustomRecurrence; touch?: boolean }) {
|
|
30
|
+
const [rule, setRule] = useState(seed);
|
|
31
|
+
return (
|
|
32
|
+
<div className="flex max-w-sm flex-col gap-4 rounded-lg border border-line bg-surface p-5">
|
|
33
|
+
<h3 className="text-lg font-semibold text-fg">Custom recurrence</h3>
|
|
34
|
+
<CustomRecurrenceEditor
|
|
35
|
+
value={rule}
|
|
36
|
+
onChange={setRule}
|
|
37
|
+
date={DATE}
|
|
38
|
+
suggestedEndDate={defaultEndDate(DATE)}
|
|
39
|
+
touch={touch}
|
|
40
|
+
/>
|
|
41
|
+
<p className="border-t border-line pt-3 text-sm text-fg-muted">
|
|
42
|
+
{formatCustomRecurrence(rule, DATE, START)}
|
|
43
|
+
</p>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What it opens as: every week, on the day the event already sits on. */
|
|
49
|
+
export const Weekly: Story = {
|
|
50
|
+
render: () => <Editor seed={defaultCustomRecurrence(DATE)} />,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Two days a week, fortnightly, until October. None of that is expressible as
|
|
55
|
+
* a choice derived from a date, which is the whole reason this exists.
|
|
56
|
+
*/
|
|
57
|
+
export const EveryOtherWeekUntilOctober: Story = {
|
|
58
|
+
name: "Every other week, until October",
|
|
59
|
+
render: () => (
|
|
60
|
+
<Editor
|
|
61
|
+
seed={{
|
|
62
|
+
interval: 2,
|
|
63
|
+
unit: "week",
|
|
64
|
+
weekdays: [1, 4],
|
|
65
|
+
monthlyMode: "dayOfMonth",
|
|
66
|
+
ends: { kind: "onDate", date: "2026-10-03" },
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A monthly rule means two different things about the same date — the ninth,
|
|
74
|
+
* or the second Tuesday — and which one is meant is never derivable. So it is
|
|
75
|
+
* asked rather than assumed.
|
|
76
|
+
*/
|
|
77
|
+
export const MonthlyReadTwoWays: Story = {
|
|
78
|
+
name: "Monthly, read two ways",
|
|
79
|
+
render: () => (
|
|
80
|
+
<Editor
|
|
81
|
+
seed={{
|
|
82
|
+
...defaultCustomRecurrence(DATE),
|
|
83
|
+
unit: "month",
|
|
84
|
+
monthlyMode: "weekdayOfMonth",
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** An ending counted rather than dated: thirteen times and then it stops. */
|
|
91
|
+
export const EndsAfterCount: Story = {
|
|
92
|
+
name: "Ends after a count",
|
|
93
|
+
render: () => (
|
|
94
|
+
<Editor
|
|
95
|
+
seed={{
|
|
96
|
+
...defaultCustomRecurrence(DATE),
|
|
97
|
+
ends: { kind: "afterCount", count: 13 },
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** The same controls at thumb size, as the phone step renders them. */
|
|
104
|
+
export const Touch: Story = {
|
|
105
|
+
render: () => <Editor seed={defaultCustomRecurrence(DATE)} touch />,
|
|
106
|
+
};
|