@tonycodes-ui/mortar 0.0.0-stage → 0.11.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 (104) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +276 -0
  3. package/CONTRIBUTING.md +48 -0
  4. package/DESIGN.md +51 -0
  5. package/LICENSE +21 -0
  6. package/README.md +155 -2
  7. package/TESTING.md +83 -0
  8. package/baseline-policy.json +16 -0
  9. package/bin/mortar.mjs +258 -0
  10. package/catalog.json +9703 -0
  11. package/cli/audit.mjs +943 -0
  12. package/cli/data.mjs +176 -0
  13. package/cli/mcp.mjs +216 -0
  14. package/cli/migrations.mjs +128 -0
  15. package/cli/packet.mjs +267 -0
  16. package/cli/search.mjs +246 -0
  17. package/cli/tools.mjs +229 -0
  18. package/design-principles.json +30 -0
  19. package/dist/agent-data.json +1 -0
  20. package/dist/application.d.ts +340 -0
  21. package/dist/application.js +240 -0
  22. package/dist/auth.d.ts +121 -0
  23. package/dist/auth.js +97 -0
  24. package/dist/brand.d.ts +69 -0
  25. package/dist/brand.js +266 -0
  26. package/dist/calendar.d.ts +75 -0
  27. package/dist/calendar.js +310 -0
  28. package/dist/choice.d.ts +36 -0
  29. package/dist/choice.js +36 -0
  30. package/dist/composer.d.ts +28 -0
  31. package/dist/composer.js +47 -0
  32. package/dist/display.d.ts +136 -0
  33. package/dist/display.js +75 -0
  34. package/dist/docs.d.ts +145 -0
  35. package/dist/docs.js +141 -0
  36. package/dist/ecommerce.d.ts +261 -0
  37. package/dist/ecommerce.js +177 -0
  38. package/dist/feedback.d.ts +80 -0
  39. package/dist/feedback.js +98 -0
  40. package/dist/foundation.d.ts +42 -0
  41. package/dist/foundation.js +121 -0
  42. package/dist/heading.d.ts +5 -0
  43. package/dist/heading.js +9 -0
  44. package/dist/icons.d.ts +50 -0
  45. package/dist/icons.js +70 -0
  46. package/dist/index.d.ts +34 -0
  47. package/dist/index.js +17 -0
  48. package/dist/inspection.d.ts +24 -0
  49. package/dist/inspection.js +14 -0
  50. package/dist/internal.d.ts +492 -0
  51. package/dist/internal.js +273 -0
  52. package/dist/layout.d.ts +107 -0
  53. package/dist/layout.js +66 -0
  54. package/dist/marketing-sections.d.ts +161 -0
  55. package/dist/marketing-sections.js +58 -0
  56. package/dist/marketing.d.ts +254 -0
  57. package/dist/marketing.js +150 -0
  58. package/dist/navigation.d.ts +35 -0
  59. package/dist/navigation.js +44 -0
  60. package/dist/primitives.d.ts +258 -0
  61. package/dist/primitives.js +244 -0
  62. package/dist/showcase.d.ts +29 -0
  63. package/dist/showcase.js +10 -0
  64. package/dist/workspace.d.ts +80 -0
  65. package/dist/workspace.js +48 -0
  66. package/docs/agents.md +127 -0
  67. package/docs/auth-flows.md +71 -0
  68. package/docs/migrating-to-0.6.md +94 -0
  69. package/docs/theming.md +170 -0
  70. package/package.json +94 -4
  71. package/skills/mortar/SKILL.md +36 -0
  72. package/src/application.css +1487 -0
  73. package/src/application.tsx +1413 -0
  74. package/src/auth.tsx +452 -0
  75. package/src/brand.ts +335 -0
  76. package/src/calendar.css +468 -0
  77. package/src/calendar.tsx +868 -0
  78. package/src/choice.tsx +240 -0
  79. package/src/composer.tsx +158 -0
  80. package/src/data.css +690 -0
  81. package/src/display.tsx +452 -0
  82. package/src/docs.css +704 -0
  83. package/src/docs.tsx +737 -0
  84. package/src/ecommerce.css +810 -0
  85. package/src/ecommerce.tsx +1033 -0
  86. package/src/feedback.tsx +388 -0
  87. package/src/foundation.css +1867 -0
  88. package/src/foundation.tsx +227 -0
  89. package/src/heading.ts +13 -0
  90. package/src/icons.tsx +237 -0
  91. package/src/index.ts +230 -0
  92. package/src/inspection.tsx +91 -0
  93. package/src/internal.tsx +614 -0
  94. package/src/layout.tsx +342 -0
  95. package/src/marketing-sections.tsx +621 -0
  96. package/src/marketing.css +2400 -0
  97. package/src/marketing.tsx +1156 -0
  98. package/src/navigation.tsx +118 -0
  99. package/src/overlays.css +350 -0
  100. package/src/primitives.tsx +1135 -0
  101. package/src/showcase.tsx +120 -0
  102. package/src/tokens.css +449 -0
  103. package/src/workspace.tsx +333 -0
  104. package/styles.css +56 -0
package/src/choice.tsx ADDED
@@ -0,0 +1,240 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import {
3
+ Field,
4
+ Label,
5
+ Description,
6
+ Listbox as HeadlessListbox,
7
+ ListboxButton,
8
+ ListboxOptions,
9
+ ListboxOption,
10
+ Combobox as HeadlessCombobox,
11
+ ComboboxInput,
12
+ ComboboxButton,
13
+ ComboboxOptions,
14
+ ComboboxOption,
15
+ } from "@headlessui/react";
16
+ import { Check, ChevronDown } from "./icons.js";
17
+ import { RequiredMark, componentRoot, cx } from "./internal.js";
18
+
19
+ export interface ChoiceOption {
20
+ value: string;
21
+ label: string;
22
+ description?: string;
23
+ /** Avatar, icon or status dot shown before the label. */
24
+ leading?: ReactNode;
25
+ disabled?: boolean;
26
+ }
27
+
28
+ interface ChoiceProps {
29
+ label: string;
30
+ options: ChoiceOption[];
31
+ value: string | null;
32
+ onChange: (value: string | null) => void;
33
+ placeholder?: string;
34
+ hint?: string;
35
+ /** Marks the field invalid; the message is announced when it appears. */
36
+ error?: string;
37
+ disabled?: boolean;
38
+ /** Marks the field required and adds a decorative marker to the label. */
39
+ required?: boolean;
40
+ /** On the field's root element. */
41
+ className?: string;
42
+ }
43
+
44
+ function OptionContent({ option }: { option: ChoiceOption }) {
45
+ return (
46
+ <>
47
+ {option.leading && (
48
+ <span className="m-choice-leading" aria-hidden="true">
49
+ {option.leading}
50
+ </span>
51
+ )}
52
+ <span className="m-choice-text">
53
+ <span>{option.label}</span>
54
+ {option.description && <small>{option.description}</small>}
55
+ </span>
56
+ <Check className="m-choice-check" />
57
+ </>
58
+ );
59
+ }
60
+
61
+ /** Label with the required marker; `spoken` adds "required" for controls that cannot carry aria-required. */
62
+ function ChoiceLabel({
63
+ label,
64
+ required,
65
+ spoken = false,
66
+ }: {
67
+ label: string;
68
+ required?: boolean;
69
+ spoken?: boolean;
70
+ }) {
71
+ return (
72
+ <Label>
73
+ {label}
74
+ {required && <RequiredMark />}
75
+ {required && spoken && <span className="m-sr-only"> (required)</span>}
76
+ </Label>
77
+ );
78
+ }
79
+
80
+ /** The hint or error; the error is a separate alert so it is announced when it appears. */
81
+ function Message({ hint, error }: { hint?: string; error?: string }) {
82
+ if (error)
83
+ return (
84
+ <Description key="error" className="m-error" role="alert">
85
+ {error}
86
+ </Description>
87
+ );
88
+ return hint ? <Description key="hint">{hint}</Description> : null;
89
+ }
90
+
91
+ /** Styled single select for rich options (avatars, descriptions). Use Select for plain native choices. */
92
+ export function Listbox({
93
+ label,
94
+ options,
95
+ value,
96
+ onChange,
97
+ placeholder = "Select…",
98
+ hint,
99
+ error,
100
+ disabled = false,
101
+ required,
102
+ className,
103
+ }: ChoiceProps) {
104
+ const selected = options.find((o) => o.value === value);
105
+ return (
106
+ <Field
107
+ {...componentRoot("Listbox")}
108
+ className={cx("m-field", className)}
109
+ disabled={disabled}
110
+ >
111
+ <ChoiceLabel label={label} required={required} spoken />
112
+ <HeadlessListbox value={value} onChange={onChange}>
113
+ <ListboxButton
114
+ className="m-choice-control"
115
+ aria-invalid={error ? true : undefined}
116
+ >
117
+ {selected ? (
118
+ <span className="m-choice-value">
119
+ {selected.leading && (
120
+ <span className="m-choice-leading" aria-hidden="true">
121
+ {selected.leading}
122
+ </span>
123
+ )}
124
+ <span>{selected.label}</span>
125
+ </span>
126
+ ) : (
127
+ <span className="m-placeholder">{placeholder}</span>
128
+ )}
129
+ <ChevronDown />
130
+ </ListboxButton>
131
+ <ListboxOptions anchor="bottom start" className="m-choice-options">
132
+ {options.map((option) => (
133
+ <ListboxOption
134
+ key={option.value}
135
+ value={option.value}
136
+ disabled={option.disabled}
137
+ className="m-choice-option"
138
+ >
139
+ <OptionContent option={option} />
140
+ </ListboxOption>
141
+ ))}
142
+ </ListboxOptions>
143
+ </HeadlessListbox>
144
+ <Message hint={hint} error={error} />
145
+ </Field>
146
+ );
147
+ }
148
+
149
+ /** Searchable single select; filters options by label as the person types. */
150
+ export function Combobox({
151
+ label,
152
+ options,
153
+ value,
154
+ onChange,
155
+ placeholder,
156
+ hint,
157
+ error,
158
+ disabled = false,
159
+ required,
160
+ className,
161
+ empty = { title: "No matches" },
162
+ }: ChoiceProps & {
163
+ /** Shown and announced when the typed text matches no option. */
164
+ empty?: { title: string; description?: string; action?: ReactNode };
165
+ }) {
166
+ const [query, setQuery] = useState("");
167
+ const needle = query.trim().toLowerCase();
168
+ const filtered = needle
169
+ ? options.filter((o) => o.label.toLowerCase().includes(needle))
170
+ : options;
171
+ const leading = query
172
+ ? undefined
173
+ : options.find((o) => o.value === value)?.leading;
174
+ const nothing = !!needle && !filtered.length;
175
+ return (
176
+ <Field
177
+ {...componentRoot("Combobox")}
178
+ className={cx("m-field", className)}
179
+ disabled={disabled}
180
+ >
181
+ <ChoiceLabel label={label} required={required} />
182
+ <HeadlessCombobox
183
+ value={value}
184
+ onChange={onChange}
185
+ onClose={() => setQuery("")}
186
+ >
187
+ <div className="m-combobox" data-leading={leading ? true : undefined}>
188
+ {leading && (
189
+ <span className="m-choice-leading" aria-hidden="true">
190
+ {leading}
191
+ </span>
192
+ )}
193
+ <ComboboxInput
194
+ className="m-choice-control"
195
+ placeholder={placeholder}
196
+ aria-invalid={error ? true : undefined}
197
+ aria-required={required || undefined}
198
+ displayValue={(v: string | null) =>
199
+ options.find((o) => o.value === v)?.label ?? ""
200
+ }
201
+ onChange={(event) => setQuery(event.target.value)}
202
+ />
203
+ <ComboboxButton
204
+ className="m-combobox-toggle"
205
+ aria-label={`Show ${label} options`}
206
+ >
207
+ <ChevronDown />
208
+ </ComboboxButton>
209
+ </div>
210
+ {filtered.length > 0 && (
211
+ <ComboboxOptions
212
+ anchor="bottom start"
213
+ className="m-choice-options m-combobox-options"
214
+ >
215
+ {filtered.map((option) => (
216
+ <ComboboxOption
217
+ key={option.value}
218
+ value={option.value}
219
+ disabled={option.disabled}
220
+ className="m-choice-option"
221
+ >
222
+ <OptionContent option={option} />
223
+ </ComboboxOption>
224
+ ))}
225
+ </ComboboxOptions>
226
+ )}
227
+ </HeadlessCombobox>
228
+ <p role="status" className={nothing ? undefined : "m-sr-only"}>
229
+ {nothing && (
230
+ <>
231
+ {empty.title}
232
+ {empty.description && <span> {empty.description}</span>}
233
+ </>
234
+ )}
235
+ </p>
236
+ {nothing && empty.action}
237
+ <Message hint={hint} error={error} />
238
+ </Field>
239
+ );
240
+ }
@@ -0,0 +1,158 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { Button, Tabs } from "./primitives.js";
3
+ import { FieldFeedback, componentRoot, cx, useField } from "./internal.js";
4
+
5
+ /** Characters left at which the count starts being announced. */
6
+ const nearLimit = (maxLength: number) =>
7
+ Math.max(10, Math.ceil(maxLength * 0.1));
8
+
9
+ /** Comment box with optional avatar, toolbar, Write/Preview tabs and a character count. */
10
+ export function CommentComposer({
11
+ onSubmit,
12
+ value: controlled,
13
+ onChange,
14
+ label = "Add your comment",
15
+ labelVisible = false,
16
+ placeholder = "Add your comment…",
17
+ submitLabel = "Comment",
18
+ loading = false,
19
+ avatar,
20
+ actions,
21
+ preview,
22
+ maxLength,
23
+ hint,
24
+ error,
25
+ rows = 3,
26
+ className,
27
+ }: {
28
+ onSubmit: (value: string) => void;
29
+ /** Controlled text; omit to let the composer manage and clear it. */
30
+ value?: string;
31
+ /** Called with the text on every edit. */
32
+ onChange?: (value: string) => void;
33
+ label?: string;
34
+ labelVisible?: boolean;
35
+ placeholder?: string;
36
+ submitLabel?: string;
37
+ /** Busy submitting: the submit button keeps focus but ignores activation. */
38
+ loading?: boolean;
39
+ /** Author avatar shown beside the box (decorative). */
40
+ avatar?: ReactNode;
41
+ /** Toolbar controls such as attach or emoji buttons. */
42
+ actions?: ReactNode;
43
+ /** Renders a preview of the text and enables Write/Preview tabs. */
44
+ preview?: (value: string) => ReactNode;
45
+ /** Shows a visible count; the remaining characters are announced only near the limit. */
46
+ maxLength?: number;
47
+ hint?: string;
48
+ /** Marks the text invalid; the message is announced when it appears. */
49
+ error?: string;
50
+ rows?: number;
51
+ className?: string;
52
+ }) {
53
+ const { fieldId: id, messageId, describedBy } = useField({ hint, error });
54
+ const [own, setOwn] = useState("");
55
+ const [mode, setMode] = useState("write");
56
+ const value = controlled ?? own;
57
+ const setValue = (next: string) => {
58
+ if (controlled === undefined) setOwn(next);
59
+ onChange?.(next);
60
+ };
61
+ const empty = !value.trim();
62
+ const submit = () => {
63
+ if (empty || loading) return;
64
+ onSubmit(value);
65
+ if (controlled === undefined) setOwn("");
66
+ };
67
+ const remaining = maxLength === undefined ? 0 : maxLength - value.length;
68
+ const field = (
69
+ <textarea
70
+ id={id}
71
+ rows={rows}
72
+ value={value}
73
+ placeholder={placeholder}
74
+ maxLength={maxLength}
75
+ aria-invalid={error ? true : undefined}
76
+ aria-describedby={describedBy}
77
+ aria-keyshortcuts="Control+Enter Meta+Enter"
78
+ onChange={(event) => setValue(event.target.value)}
79
+ onKeyDown={(event) => {
80
+ if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
81
+ event.preventDefault();
82
+ submit();
83
+ }
84
+ }}
85
+ />
86
+ );
87
+ return (
88
+ <form
89
+ {...componentRoot("CommentComposer")}
90
+ className={cx("m-composer", className)}
91
+ onSubmit={(event) => {
92
+ event.preventDefault();
93
+ submit();
94
+ }}
95
+ >
96
+ {avatar && (
97
+ <span className="m-composer-avatar" aria-hidden="true">
98
+ {avatar}
99
+ </span>
100
+ )}
101
+ <div className="m-composer-main">
102
+ <label
103
+ className={labelVisible ? "m-composer-label" : "m-sr-only"}
104
+ htmlFor={id}
105
+ >
106
+ {label}
107
+ </label>
108
+ <div className="m-composer-box" data-invalid={error ? true : undefined}>
109
+ {preview ? (
110
+ <Tabs
111
+ label="Comment mode"
112
+ value={mode}
113
+ onChange={setMode}
114
+ items={[
115
+ { id: "write", label: "Write", content: field },
116
+ {
117
+ id: "preview",
118
+ label: "Preview",
119
+ content: (
120
+ <div className="m-composer-preview">
121
+ {empty ? (
122
+ <p className="m-composer-empty">Nothing to preview</p>
123
+ ) : (
124
+ preview(value)
125
+ )}
126
+ </div>
127
+ ),
128
+ },
129
+ ]}
130
+ />
131
+ ) : (
132
+ field
133
+ )}
134
+ <div className="m-composer-bar">
135
+ {actions && <div className="m-composer-actions">{actions}</div>}
136
+ {maxLength !== undefined && (
137
+ <span className="m-composer-count">
138
+ {value.length} / {maxLength}
139
+ </span>
140
+ )}
141
+ <Button type="submit" disabled={empty} loading={loading}>
142
+ {submitLabel}
143
+ </Button>
144
+ {maxLength !== undefined && (
145
+ <span className="m-sr-only" role="status">
146
+ {remaining <= nearLimit(maxLength) &&
147
+ (remaining > 0
148
+ ? `${remaining} ${remaining === 1 ? "character" : "characters"} left`
149
+ : "Character limit reached")}
150
+ </span>
151
+ )}
152
+ </div>
153
+ </div>
154
+ <FieldFeedback id={messageId} hint={hint} error={error} />
155
+ </div>
156
+ </form>
157
+ );
158
+ }