@tonycodes-ui/mortar 0.0.0-stage → 0.12.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/AGENTS.md +10 -0
- package/CHANGELOG.md +268 -0
- package/CONTRIBUTING.md +60 -0
- package/DESIGN.md +51 -0
- package/LICENSE +21 -0
- package/README.md +155 -2
- package/TESTING.md +83 -0
- package/baseline-policy.json +16 -0
- package/bin/mortar.mjs +258 -0
- package/catalog.json +9900 -0
- package/cli/audit.mjs +943 -0
- package/cli/data.mjs +176 -0
- package/cli/mcp.mjs +216 -0
- package/cli/migrations.mjs +128 -0
- package/cli/packet.mjs +267 -0
- package/cli/search.mjs +246 -0
- package/cli/tools.mjs +229 -0
- package/design-principles.json +30 -0
- package/dist/agent-data.json +1 -0
- package/dist/application.d.ts +340 -0
- package/dist/application.js +240 -0
- package/dist/auth.d.ts +121 -0
- package/dist/auth.js +97 -0
- package/dist/brand.d.ts +69 -0
- package/dist/brand.js +266 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.js +310 -0
- package/dist/choice.d.ts +36 -0
- package/dist/choice.js +36 -0
- package/dist/composer.d.ts +28 -0
- package/dist/composer.js +47 -0
- package/dist/display.d.ts +136 -0
- package/dist/display.js +75 -0
- package/dist/docs.d.ts +145 -0
- package/dist/docs.js +141 -0
- package/dist/ecommerce.d.ts +261 -0
- package/dist/ecommerce.js +177 -0
- package/dist/feedback.d.ts +80 -0
- package/dist/feedback.js +98 -0
- package/dist/foundation.d.ts +42 -0
- package/dist/foundation.js +121 -0
- package/dist/heading.d.ts +5 -0
- package/dist/heading.js +9 -0
- package/dist/icons.d.ts +50 -0
- package/dist/icons.js +70 -0
- package/dist/illustration.d.ts +47 -0
- package/dist/illustration.js +47 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +18 -0
- package/dist/inspection.d.ts +24 -0
- package/dist/inspection.js +14 -0
- package/dist/internal.d.ts +514 -0
- package/dist/internal.js +332 -0
- package/dist/layout.d.ts +107 -0
- package/dist/layout.js +66 -0
- package/dist/marketing-sections.d.ts +161 -0
- package/dist/marketing-sections.js +58 -0
- package/dist/marketing.d.ts +277 -0
- package/dist/marketing.js +163 -0
- package/dist/navigation.d.ts +35 -0
- package/dist/navigation.js +44 -0
- package/dist/primitives.d.ts +258 -0
- package/dist/primitives.js +244 -0
- package/dist/showcase.d.ts +29 -0
- package/dist/showcase.js +10 -0
- package/dist/workspace.d.ts +80 -0
- package/dist/workspace.js +48 -0
- package/docs/agents.md +127 -0
- package/docs/auth-flows.md +71 -0
- package/docs/migrating-to-0.6.md +94 -0
- package/docs/theming.md +170 -0
- package/package.json +94 -4
- package/skills/mortar/SKILL.md +36 -0
- package/src/application.css +1487 -0
- package/src/application.tsx +1413 -0
- package/src/auth.tsx +452 -0
- package/src/brand.ts +335 -0
- package/src/calendar.css +468 -0
- package/src/calendar.tsx +868 -0
- package/src/choice.tsx +240 -0
- package/src/composer.tsx +158 -0
- package/src/data.css +690 -0
- package/src/display.tsx +452 -0
- package/src/docs.css +704 -0
- package/src/docs.tsx +737 -0
- package/src/ecommerce.css +810 -0
- package/src/ecommerce.tsx +1033 -0
- package/src/feedback.tsx +388 -0
- package/src/foundation.css +1867 -0
- package/src/foundation.tsx +227 -0
- package/src/heading.ts +13 -0
- package/src/icons.tsx +237 -0
- package/src/illustration.tsx +169 -0
- package/src/index.ts +233 -0
- package/src/inspection.tsx +91 -0
- package/src/internal.tsx +700 -0
- package/src/layout.tsx +342 -0
- package/src/marketing-sections.tsx +621 -0
- package/src/marketing.css +2579 -0
- package/src/marketing.tsx +1235 -0
- package/src/navigation.tsx +118 -0
- package/src/overlays.css +350 -0
- package/src/primitives.tsx +1135 -0
- package/src/showcase.tsx +120 -0
- package/src/tokens.css +449 -0
- package/src/workspace.tsx +333 -0
- package/styles.css +56 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { KitLink, type ItemGroup } from "./navigation.js";
|
|
2
|
+
import { Fragment, useId, type ReactNode } from "react";
|
|
3
|
+
import {
|
|
4
|
+
Combobox,
|
|
5
|
+
ComboboxInput,
|
|
6
|
+
ComboboxOption,
|
|
7
|
+
ComboboxOptions,
|
|
8
|
+
Description,
|
|
9
|
+
DialogPanel,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from "@headlessui/react";
|
|
12
|
+
import { Button } from "./primitives.js";
|
|
13
|
+
import { Kbd } from "./display.js";
|
|
14
|
+
import { ChevronRight, Close } from "./icons.js";
|
|
15
|
+
import { childLevel, headingTag, type HeadingLevel } from "./heading.js";
|
|
16
|
+
import {
|
|
17
|
+
DialogFrame,
|
|
18
|
+
componentRoot,
|
|
19
|
+
cx,
|
|
20
|
+
useComponentName,
|
|
21
|
+
} from "./internal.js";
|
|
22
|
+
|
|
23
|
+
export interface SlideOverProps {
|
|
24
|
+
open: boolean;
|
|
25
|
+
onClose: () => void;
|
|
26
|
+
title: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
children: ReactNode;
|
|
29
|
+
actions?: ReactNode;
|
|
30
|
+
/** Panel width: md (default), lg or xl. */
|
|
31
|
+
size?: "md" | "lg" | "xl";
|
|
32
|
+
/** Class on the sliding panel. */
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
export function SlideOver({
|
|
36
|
+
open,
|
|
37
|
+
onClose,
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
children,
|
|
41
|
+
actions,
|
|
42
|
+
size = "md",
|
|
43
|
+
className,
|
|
44
|
+
}: SlideOverProps) {
|
|
45
|
+
return (
|
|
46
|
+
<DialogFrame open={open} onClose={onClose} component="SlideOver">
|
|
47
|
+
<DialogPanel
|
|
48
|
+
className={cx("m-slide-over", className)}
|
|
49
|
+
data-size={size === "md" ? undefined : size}
|
|
50
|
+
>
|
|
51
|
+
<header>
|
|
52
|
+
<div>
|
|
53
|
+
<DialogTitle>{title}</DialogTitle>
|
|
54
|
+
{description && <Description>{description}</Description>}
|
|
55
|
+
</div>
|
|
56
|
+
<Button variant="ghost" onClick={onClose} aria-label="Close panel">
|
|
57
|
+
<Close />
|
|
58
|
+
</Button>
|
|
59
|
+
</header>
|
|
60
|
+
<div className="m-slide-over-body">{children}</div>
|
|
61
|
+
{actions && <footer className="m-actions">{actions}</footer>}
|
|
62
|
+
</DialogPanel>
|
|
63
|
+
</DialogFrame>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CommandItem {
|
|
68
|
+
id: string;
|
|
69
|
+
label: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
meta?: string;
|
|
72
|
+
shortcut?: string[];
|
|
73
|
+
/** Icon or avatar before the label. */
|
|
74
|
+
icon?: ReactNode;
|
|
75
|
+
/** [start, end) character range of label to emphasise as the query match. */
|
|
76
|
+
match?: [number, number];
|
|
77
|
+
onSelect: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function Highlight({
|
|
81
|
+
label,
|
|
82
|
+
match,
|
|
83
|
+
}: {
|
|
84
|
+
label: string;
|
|
85
|
+
match?: [number, number];
|
|
86
|
+
}) {
|
|
87
|
+
if (!match) return <>{label}</>;
|
|
88
|
+
const [a, b] = match;
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
{label.slice(0, a)}
|
|
92
|
+
<mark>{label.slice(a, b)}</mark>
|
|
93
|
+
{label.slice(b)}
|
|
94
|
+
</>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CommandPaletteProps {
|
|
99
|
+
open: boolean;
|
|
100
|
+
onClose: () => void;
|
|
101
|
+
query: string;
|
|
102
|
+
onQueryChange: (value: string) => void;
|
|
103
|
+
groups: ItemGroup<CommandItem>[];
|
|
104
|
+
label?: string;
|
|
105
|
+
placeholder?: string;
|
|
106
|
+
/** Announced when no group has items; defaults to "No results for “query”.". */
|
|
107
|
+
empty?: { title: string; description?: string };
|
|
108
|
+
footer?: ReactNode;
|
|
109
|
+
/** Class on the palette panel. */
|
|
110
|
+
className?: string;
|
|
111
|
+
}
|
|
112
|
+
/** Controlled ⌘K palette. Filtering and ranking stay with the consumer. */
|
|
113
|
+
export function CommandPalette({
|
|
114
|
+
open,
|
|
115
|
+
onClose,
|
|
116
|
+
query,
|
|
117
|
+
onQueryChange,
|
|
118
|
+
groups,
|
|
119
|
+
label = "Command menu",
|
|
120
|
+
placeholder = "Type a command or search",
|
|
121
|
+
empty,
|
|
122
|
+
footer,
|
|
123
|
+
className,
|
|
124
|
+
}: CommandPaletteProps) {
|
|
125
|
+
const total = groups.reduce((n, g) => n + g.items.length, 0);
|
|
126
|
+
const component = useComponentName("CommandPalette");
|
|
127
|
+
return (
|
|
128
|
+
<DialogFrame open={open} onClose={onClose} component={component}>
|
|
129
|
+
<div className="m-dialog-position m-palette-position">
|
|
130
|
+
<DialogPanel className={cx("m-palette", className)}>
|
|
131
|
+
<DialogTitle className="m-sr-only">{label}</DialogTitle>
|
|
132
|
+
<Combobox<CommandItem | null>
|
|
133
|
+
value={null}
|
|
134
|
+
onChange={(item) => {
|
|
135
|
+
if (!item) return;
|
|
136
|
+
item.onSelect();
|
|
137
|
+
onClose();
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<div className="m-palette-input">
|
|
141
|
+
<ComboboxInput
|
|
142
|
+
autoFocus
|
|
143
|
+
aria-label={label}
|
|
144
|
+
placeholder={placeholder}
|
|
145
|
+
value={query}
|
|
146
|
+
onChange={(e) => onQueryChange(e.target.value)}
|
|
147
|
+
onKeyDownCapture={(e) => {
|
|
148
|
+
if (e.key === "Escape") {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
onClose();
|
|
152
|
+
}
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
<Kbd keys={["esc"]} />
|
|
156
|
+
</div>
|
|
157
|
+
<ComboboxOptions static modal={false} className="m-palette-results">
|
|
158
|
+
{groups
|
|
159
|
+
.filter((g) => g.items.length)
|
|
160
|
+
.map((group) => (
|
|
161
|
+
<div role="group" aria-label={group.label} key={group.label}>
|
|
162
|
+
<div className="m-palette-group" aria-hidden="true">
|
|
163
|
+
{group.label}
|
|
164
|
+
</div>
|
|
165
|
+
{group.items.map((item) => (
|
|
166
|
+
<ComboboxOption key={item.id} value={item} as={Fragment}>
|
|
167
|
+
<div className="m-palette-item">
|
|
168
|
+
{item.icon && (
|
|
169
|
+
<span className="m-palette-icon" aria-hidden="true">
|
|
170
|
+
{item.icon}
|
|
171
|
+
</span>
|
|
172
|
+
)}
|
|
173
|
+
<span>
|
|
174
|
+
<span className="m-palette-label">
|
|
175
|
+
<Highlight
|
|
176
|
+
label={item.label}
|
|
177
|
+
match={item.match}
|
|
178
|
+
/>
|
|
179
|
+
</span>
|
|
180
|
+
{item.description && (
|
|
181
|
+
<small>{item.description}</small>
|
|
182
|
+
)}
|
|
183
|
+
</span>
|
|
184
|
+
{item.shortcut ? (
|
|
185
|
+
<Kbd keys={item.shortcut} />
|
|
186
|
+
) : (
|
|
187
|
+
item.meta && (
|
|
188
|
+
<span className="m-palette-meta">
|
|
189
|
+
{item.meta}
|
|
190
|
+
</span>
|
|
191
|
+
)
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
</ComboboxOption>
|
|
195
|
+
))}
|
|
196
|
+
</div>
|
|
197
|
+
))}
|
|
198
|
+
</ComboboxOptions>
|
|
199
|
+
</Combobox>
|
|
200
|
+
{/* Always mounted, so "no results" is announced when it appears.
|
|
201
|
+
It sits outside the listbox, which only allows options; the
|
|
202
|
+
options are not modal so this stays exposed (the dialog already
|
|
203
|
+
is). */}
|
|
204
|
+
<p className={total ? "m-sr-only" : "m-palette-empty"} role="status">
|
|
205
|
+
{total ? (
|
|
206
|
+
""
|
|
207
|
+
) : empty ? (
|
|
208
|
+
<>
|
|
209
|
+
{empty.title}
|
|
210
|
+
{empty.description && <span> {empty.description}</span>}
|
|
211
|
+
</>
|
|
212
|
+
) : (
|
|
213
|
+
`No results for “${query}”.`
|
|
214
|
+
)}
|
|
215
|
+
</p>
|
|
216
|
+
{footer && <div className="m-palette-footer">{footer}</div>}
|
|
217
|
+
</DialogPanel>
|
|
218
|
+
</div>
|
|
219
|
+
</DialogFrame>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface StackedListItem {
|
|
224
|
+
id: string;
|
|
225
|
+
title: ReactNode;
|
|
226
|
+
description?: ReactNode;
|
|
227
|
+
leading?: ReactNode;
|
|
228
|
+
trailing?: ReactNode;
|
|
229
|
+
/**
|
|
230
|
+
* Second column at the end of the row, e.g. a role and "Last seen 3h ago";
|
|
231
|
+
* it wraps under the text when narrow. It sits inside the row's link, so it
|
|
232
|
+
* must not contain links or buttons.
|
|
233
|
+
*/
|
|
234
|
+
meta?: ReactNode;
|
|
235
|
+
href?: string;
|
|
236
|
+
}
|
|
237
|
+
export interface StackedListProps {
|
|
238
|
+
/** Accessible name; defaults to the visible `title`. */
|
|
239
|
+
label?: string;
|
|
240
|
+
/** Visible heading above the list. */
|
|
241
|
+
title?: ReactNode;
|
|
242
|
+
/** Level of the title, or of the group headings when there is no title. Group headings sit one level below the title. */
|
|
243
|
+
headingLevel?: HeadingLevel;
|
|
244
|
+
/** Button or link in the header, e.g. "View all". */
|
|
245
|
+
action?: ReactNode;
|
|
246
|
+
/** Rows; with `groups` they render first, ungrouped. */
|
|
247
|
+
items?: StackedListItem[];
|
|
248
|
+
/** Sections with sticky headings, e.g. a directory grouped by letter. */
|
|
249
|
+
groups?: ItemGroup<StackedListItem>[];
|
|
250
|
+
/** Ends each row that has an `href` with a chevron, so rows read as links. */
|
|
251
|
+
chevrons?: boolean;
|
|
252
|
+
id?: string;
|
|
253
|
+
className?: string;
|
|
254
|
+
}
|
|
255
|
+
export function StackedList({
|
|
256
|
+
label,
|
|
257
|
+
title,
|
|
258
|
+
headingLevel = 2,
|
|
259
|
+
action,
|
|
260
|
+
items = [],
|
|
261
|
+
groups = [],
|
|
262
|
+
chevrons = false,
|
|
263
|
+
id,
|
|
264
|
+
className,
|
|
265
|
+
}: StackedListProps) {
|
|
266
|
+
const base = useId();
|
|
267
|
+
const hasTitle = title !== undefined && title !== null && title !== "";
|
|
268
|
+
const Title = headingTag(headingLevel);
|
|
269
|
+
const GroupHeading = headingTag(
|
|
270
|
+
hasTitle ? childLevel(headingLevel) : headingLevel,
|
|
271
|
+
);
|
|
272
|
+
const row = (item: StackedListItem) => {
|
|
273
|
+
const body = (
|
|
274
|
+
<>
|
|
275
|
+
{item.leading}
|
|
276
|
+
<span className="m-stacked-text">
|
|
277
|
+
<strong>{item.title}</strong>
|
|
278
|
+
{item.description && <span>{item.description}</span>}
|
|
279
|
+
</span>
|
|
280
|
+
{item.meta && <span className="m-stacked-meta">{item.meta}</span>}
|
|
281
|
+
{chevrons && item.href && (
|
|
282
|
+
<span className="m-stacked-chevron" aria-hidden="true">
|
|
283
|
+
<ChevronRight />
|
|
284
|
+
</span>
|
|
285
|
+
)}
|
|
286
|
+
</>
|
|
287
|
+
);
|
|
288
|
+
return (
|
|
289
|
+
<li key={item.id}>
|
|
290
|
+
{item.href ? (
|
|
291
|
+
<KitLink href={item.href}>{body}</KitLink>
|
|
292
|
+
) : (
|
|
293
|
+
<div>{body}</div>
|
|
294
|
+
)}
|
|
295
|
+
{item.trailing && (
|
|
296
|
+
<div className="m-stacked-trailing">{item.trailing}</div>
|
|
297
|
+
)}
|
|
298
|
+
</li>
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
return (
|
|
302
|
+
<section
|
|
303
|
+
id={id}
|
|
304
|
+
{...componentRoot("StackedList")}
|
|
305
|
+
className={cx("m-stacked-list", className)}
|
|
306
|
+
aria-label={label}
|
|
307
|
+
aria-labelledby={label || !hasTitle ? undefined : `${base}-title`}
|
|
308
|
+
>
|
|
309
|
+
{(hasTitle || action) && (
|
|
310
|
+
<header>
|
|
311
|
+
{hasTitle && (
|
|
312
|
+
<Title id={`${base}-title`} className="m-stacked-title">
|
|
313
|
+
{title}
|
|
314
|
+
</Title>
|
|
315
|
+
)}
|
|
316
|
+
{action && <div className="m-stacked-action">{action}</div>}
|
|
317
|
+
</header>
|
|
318
|
+
)}
|
|
319
|
+
{(items.length > 0 || !groups.length) && <ul>{items.map(row)}</ul>}
|
|
320
|
+
{groups.map((group, index) => (
|
|
321
|
+
<div key={group.label} className="m-stacked-group">
|
|
322
|
+
<GroupHeading
|
|
323
|
+
id={`${base}-${index}`}
|
|
324
|
+
className="m-stacked-group-heading"
|
|
325
|
+
>
|
|
326
|
+
{group.label}
|
|
327
|
+
</GroupHeading>
|
|
328
|
+
<ul aria-labelledby={`${base}-${index}`}>{group.items.map(row)}</ul>
|
|
329
|
+
</div>
|
|
330
|
+
))}
|
|
331
|
+
</section>
|
|
332
|
+
);
|
|
333
|
+
}
|
package/styles.css
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
@import "./src/tokens.css";
|
|
2
|
+
/* Component styles, one section per component, grouped by family. The order of
|
|
3
|
+
these imports is part of the cascade: keep it. */
|
|
4
|
+
@import "./src/foundation.css";
|
|
5
|
+
@import "./src/overlays.css";
|
|
6
|
+
@import "./src/application.css";
|
|
7
|
+
@import "./src/data.css";
|
|
8
|
+
@import "./src/calendar.css";
|
|
9
|
+
@import "./src/marketing.css";
|
|
10
|
+
@import "./src/docs.css";
|
|
11
|
+
@import "./src/ecommerce.css";
|
|
12
|
+
@custom-variant mortar-dark (&:where([data-mortar-appearance='dark'], [data-mortar-appearance='dark'] *));
|
|
13
|
+
@theme inline {
|
|
14
|
+
--color-paper: var(--m-paper);
|
|
15
|
+
--color-surface: var(--m-surface);
|
|
16
|
+
--color-sunken: var(--m-sunken);
|
|
17
|
+
--color-ink: var(--m-ink);
|
|
18
|
+
--color-muted: var(--m-muted);
|
|
19
|
+
--color-line: var(--m-line);
|
|
20
|
+
--color-primary: var(--m-primary);
|
|
21
|
+
--color-on-primary: var(--m-on-primary);
|
|
22
|
+
--color-accent: var(--m-accent);
|
|
23
|
+
--color-on-accent: var(--m-on-accent);
|
|
24
|
+
--color-focus: var(--m-focus);
|
|
25
|
+
--color-success: var(--m-success);
|
|
26
|
+
--color-warning: var(--m-warning);
|
|
27
|
+
--color-danger: var(--m-danger);
|
|
28
|
+
--color-info: var(--m-info);
|
|
29
|
+
--color-secondary: var(--m-secondary);
|
|
30
|
+
--color-selected: var(--m-selected);
|
|
31
|
+
--color-link: var(--m-link);
|
|
32
|
+
/* A brand's fonts win where it applies; otherwise the scale tokens. */
|
|
33
|
+
--font-sans: var(--m-brand-font, var(--m-font));
|
|
34
|
+
--font-heading: var(
|
|
35
|
+
--m-brand-font-heading,
|
|
36
|
+
var(--m-font-heading, var(--m-brand-font, var(--m-font)))
|
|
37
|
+
);
|
|
38
|
+
--font-mono: var(--m-mono);
|
|
39
|
+
/* Weights equal Tailwind's defaults, so these follow a retheme without
|
|
40
|
+
changing anything until one happens. */
|
|
41
|
+
--font-weight-medium: var(--m-weight-medium);
|
|
42
|
+
--font-weight-semibold: var(--m-weight-semibold);
|
|
43
|
+
--font-weight-bold: var(--m-weight-bold);
|
|
44
|
+
/* Mortar's radii under their own names (rounded-m, rounded-m-lg, …), so
|
|
45
|
+
Tailwind's rounded-sm/md/lg keep their usual sizes. */
|
|
46
|
+
--radius-m-xs: var(--m-radius-xs);
|
|
47
|
+
--radius-m-sm: var(--m-radius-sm);
|
|
48
|
+
--radius-m: var(--m-radius);
|
|
49
|
+
--radius-m-lg: var(--m-radius-lg);
|
|
50
|
+
--radius-m-xl: var(--m-radius-xl);
|
|
51
|
+
--radius-m-full: var(--m-radius-full);
|
|
52
|
+
--shadow-float: var(--m-float);
|
|
53
|
+
--tracking-display: var(--m-tracking-display);
|
|
54
|
+
--tracking-heading: var(--m-tracking-heading);
|
|
55
|
+
--tracking-caps: var(--m-tracking-caps);
|
|
56
|
+
}
|