@gogitcms/design-system 0.15.0-next.3
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/README.md +77 -0
- package/css/components.css +312 -0
- package/css/tokens.css +212 -0
- package/package.json +65 -0
- package/src/ThemeProvider.tsx +86 -0
- package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
- package/src/__tests__/BranchImport.test.tsx +46 -0
- package/src/__tests__/Button.test.tsx +45 -0
- package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
- package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
- package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
- package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
- package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
- package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
- package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
- package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
- package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
- package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
- package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
- package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
- package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
- package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
- package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
- package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
- package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
- package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
- package/src/__tests__/MediaBrowser.test.tsx +353 -0
- package/src/__tests__/MediaField.test.tsx +185 -0
- package/src/__tests__/Notifications.test.tsx +69 -0
- package/src/__tests__/Onboarding.test.tsx +287 -0
- package/src/__tests__/cssTokens.test.ts +201 -0
- package/src/__tests__/fieldComponents.test.ts +43 -0
- package/src/__tests__/reorder.test.ts +58 -0
- package/src/components/ApplyChangesModal.tsx +348 -0
- package/src/components/BranchImport.tsx +157 -0
- package/src/components/BranchMenu.tsx +192 -0
- package/src/components/Button.tsx +131 -0
- package/src/components/ChangeDetail.tsx +472 -0
- package/src/components/ChangeRequestSummary.tsx +173 -0
- package/src/components/CollabField.tsx +388 -0
- package/src/components/ContentBrowser.tsx +5073 -0
- package/src/components/Icon.tsx +28 -0
- package/src/components/Icon.web.tsx +31 -0
- package/src/components/Input.tsx +106 -0
- package/src/components/MediaBrowser.tsx +766 -0
- package/src/components/MediaField.tsx +670 -0
- package/src/components/MediaPreview.tsx +91 -0
- package/src/components/MediaPreview.web.tsx +169 -0
- package/src/components/NavRow.tsx +105 -0
- package/src/components/Notifications.tsx +301 -0
- package/src/components/Onboarding.tsx +751 -0
- package/src/components/ProjectMenu.tsx +124 -0
- package/src/components/Segment.tsx +87 -0
- package/src/components/Skeleton.tsx +216 -0
- package/src/components/Spinner.tsx +44 -0
- package/src/components/Text.tsx +85 -0
- package/src/components/documentDrafts.ts +213 -0
- package/src/components/layout.tsx +284 -0
- package/src/components/primitives.tsx +143 -0
- package/src/components/reorder.ts +40 -0
- package/src/fieldComponents.ts +63 -0
- package/src/icons.ts +102 -0
- package/src/index.ts +198 -0
- package/src/media.ts +229 -0
- package/src/theme.ts +116 -0
- package/src/web/Button.tsx +110 -0
- package/src/web/Icon.tsx +52 -0
- package/src/web/Input.tsx +39 -0
- package/src/web/index.ts +43 -0
- package/src/web/primitives.tsx +119 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// The set of display components valid for each field type, and the default
|
|
2
|
+
// component when `display.component` is omitted.
|
|
3
|
+
//
|
|
4
|
+
// This is the TypeScript mirror of packages/importer/schema/components.go — the
|
|
5
|
+
// Go file is the source of truth validated on config import; keep the two in
|
|
6
|
+
// sync. `select` is the enum picker (string / number). date and datetime are
|
|
7
|
+
// valid for both string (ISO-8601 text) and number (epoch).
|
|
8
|
+
|
|
9
|
+
export const componentsByType: Record<string, string[]> = {
|
|
10
|
+
string: ["input", "textarea", "body", "slug", "email", "url", "date", "datetime", "select", "media"],
|
|
11
|
+
boolean: ["checkbox", "switch"],
|
|
12
|
+
integer: ["number", "stepper", "telephone", "date", "datetime", "select"],
|
|
13
|
+
float: ["number", "stepper", "telephone", "date", "datetime", "select"],
|
|
14
|
+
array: ["list", "mixedList"],
|
|
15
|
+
object: ["inlineGroup", "group", "media"],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const defaultComponentByType: Record<string, string> = {
|
|
19
|
+
string: "input",
|
|
20
|
+
boolean: "checkbox",
|
|
21
|
+
integer: "number",
|
|
22
|
+
float: "number",
|
|
23
|
+
array: "list",
|
|
24
|
+
object: "inlineGroup",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// defaultComponent returns the component to render when none is specified.
|
|
28
|
+
export function defaultComponent(type: string): string {
|
|
29
|
+
return defaultComponentByType[type] ?? "input";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// resolveComponent returns the effective component for a field: its explicit
|
|
33
|
+
// display.component, or the default for its type.
|
|
34
|
+
//
|
|
35
|
+
// A "plugin:"-prefixed component resolves to a default too. Naming one is not a
|
|
36
|
+
// claim that the client has it — the host renders the plugin ahead of these
|
|
37
|
+
// controls when it does (createPluginRenderField), and reaches here only when
|
|
38
|
+
// the plugin is absent or refused the field's type. Matching nothing in that
|
|
39
|
+
// case fell through to the single-line input, which is a poor control for a
|
|
40
|
+
// field the plugin was chosen for and a terrible one for a whole file body, so
|
|
41
|
+
// the fallback is picked from what the schema does say: `source: body` means
|
|
42
|
+
// the value is the document's prose, hence the multi-line control.
|
|
43
|
+
export function resolveComponent(field: {
|
|
44
|
+
type: string;
|
|
45
|
+
component?: string | null;
|
|
46
|
+
source?: string | null;
|
|
47
|
+
}): string {
|
|
48
|
+
if (!field.component || field.component.startsWith("plugin:")) {
|
|
49
|
+
return field.source === "body" ? "textarea" : defaultComponent(field.type);
|
|
50
|
+
}
|
|
51
|
+
return field.component;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// componentAllowed reports whether a component is valid for a field type. The
|
|
55
|
+
// server rejects invalid combinations on import; this mirror lets clients guard
|
|
56
|
+
// too. A "plugin:"-prefixed component names a client-side plugin field
|
|
57
|
+
// (docs/plugin-system.md): any named plugin component passes here — the host
|
|
58
|
+
// enforces the plugin's own type restriction at render time and falls back to
|
|
59
|
+
// the type's default control when the plugin is absent.
|
|
60
|
+
export function componentAllowed(type: string, component: string): boolean {
|
|
61
|
+
if (component.startsWith("plugin:")) return component !== "plugin:";
|
|
62
|
+
return (componentsByType[type] ?? []).includes(component);
|
|
63
|
+
}
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Lucide-style line icons (24px grid, 1.5px stroke, round joins, no fill).
|
|
2
|
+
// Each icon is a list of SVG path `d` strings so it renders identically via a
|
|
3
|
+
// raw <svg> on web and react-native-svg on native.
|
|
4
|
+
|
|
5
|
+
export const icons = {
|
|
6
|
+
file: ["M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z", "M14 2v6h6"],
|
|
7
|
+
newspaper: [
|
|
8
|
+
"M4 22a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 0 2 2z",
|
|
9
|
+
"M18 22a2 2 0 0 0 2-2",
|
|
10
|
+
"M8 8h8", "M8 12h8", "M8 16h5",
|
|
11
|
+
],
|
|
12
|
+
image: [
|
|
13
|
+
"M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Z",
|
|
14
|
+
"M9 11a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z",
|
|
15
|
+
"m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21",
|
|
16
|
+
],
|
|
17
|
+
braces: [
|
|
18
|
+
"M8 3H7a2 2 0 0 0-2 2v5a2 2 0 0 1-2 2 2 2 0 0 1 2 2v5a2 2 0 0 0 2 2h1",
|
|
19
|
+
"M16 21h1a2 2 0 0 0 2-2v-5a2 2 0 0 1 2-2 2 2 0 0 1-2-2V5a2 2 0 0 0-2-2h-1",
|
|
20
|
+
],
|
|
21
|
+
listTree: ["M21 12h-8", "M21 6H8", "M21 18h-8", "M4 6v4a2 2 0 0 0 2 2h2", "M4 10v6a2 2 0 0 0 2 2h2"],
|
|
22
|
+
settings: [
|
|
23
|
+
"M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",
|
|
24
|
+
"M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-2.82 1.17V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 8 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 14H4.5a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 6 8.6a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 10 4.6h.09A1.65 1.65 0 0 0 11 3.09V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 10h.1a2 2 0 0 1 0 4h-.1a1.65 1.65 0 0 0-1.5 1Z",
|
|
25
|
+
],
|
|
26
|
+
chevronRight: ["m9 18 6-6-6-6"],
|
|
27
|
+
chevronLeft: ["m15 18-6-6 6-6"],
|
|
28
|
+
chevronDown: ["m6 9 6 6 6-6"],
|
|
29
|
+
chevronUp: ["m18 15-6-6-6 6"],
|
|
30
|
+
folder: ["M3 6a2 2 0 0 1 2-2h5l2 2h7a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"],
|
|
31
|
+
building: ["M3 21h18", "M6 21V7l7-4v18", "M18 21V11l-5-3"],
|
|
32
|
+
menu: ["M3 6h18", "M3 12h18", "M3 18h18"],
|
|
33
|
+
plus: ["M5 12h14", "M12 5v14"],
|
|
34
|
+
sun: [
|
|
35
|
+
"M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8Z",
|
|
36
|
+
"M12 2v2", "M12 20v2", "m4.9 4.9 1.4 1.4", "m17.7 17.7 1.4 1.4",
|
|
37
|
+
"M2 12h2", "M20 12h2", "m6.3 17.7-1.4 1.4", "m19.1 4.9-1.4 1.4",
|
|
38
|
+
],
|
|
39
|
+
moon: ["M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"],
|
|
40
|
+
search: ["M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16Z", "m21 21-4.3-4.3"],
|
|
41
|
+
filter: ["M22 3H2l8 9.46V19l4 2v-8.54L22 3z"],
|
|
42
|
+
gitBranch: ["M6 3v12", "M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M18 9a9 9 0 0 1-9 9"],
|
|
43
|
+
gitCommit: ["M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M3 12h6", "M15 12h6"],
|
|
44
|
+
gitMerge: ["M6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M6 21V9a9 9 0 0 0 9 9"],
|
|
45
|
+
gitPullRequest: ["M6 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M18 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z", "M13 6h3a2 2 0 0 1 2 2v7", "M6 9v12"],
|
|
46
|
+
copy: ["M10 8h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H10a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2Z", "M4 16a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2"],
|
|
47
|
+
lock: ["M5 11h14a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2Z", "M7 11V7a5 5 0 0 1 10 0v4"],
|
|
48
|
+
panelLeft: ["M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Z", "M9 3v18"],
|
|
49
|
+
more: ["M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z", "M19 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z", "M5 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"],
|
|
50
|
+
gripVertical: [
|
|
51
|
+
"M9 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z", "M9 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z", "M9 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z",
|
|
52
|
+
"M15 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z", "M15 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z", "M15 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z",
|
|
53
|
+
],
|
|
54
|
+
x: ["M18 6 6 18", "m6 6 12 12"],
|
|
55
|
+
trash: [
|
|
56
|
+
"M3 6h18", "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",
|
|
57
|
+
"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6", "M10 11v6", "M14 11v6",
|
|
58
|
+
],
|
|
59
|
+
check: ["M20 6 9 17l-5-5"],
|
|
60
|
+
help: ["M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20Z", "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3", "M12 17h.01"],
|
|
61
|
+
history: ["M3 3v5h5", "M3.05 13A9 9 0 1 0 6 5.3L3 8", "M12 7v5l4 2"],
|
|
62
|
+
logOut: ["M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4", "m16 17 5-5-5-5", "M21 12H9"],
|
|
63
|
+
bell: [
|
|
64
|
+
"M10.268 21a2 2 0 0 0 3.464 0",
|
|
65
|
+
"M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326",
|
|
66
|
+
],
|
|
67
|
+
// Media kinds: one icon per render class, for the collapsed representation of
|
|
68
|
+
// an asset and for the file chip that stands in where no preview is possible.
|
|
69
|
+
video: ["m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5", "M2 6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2z"],
|
|
70
|
+
audio: ["M9 18V5l12-2v13", "M9 18a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z", "M21 16a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"],
|
|
71
|
+
fileText: [
|
|
72
|
+
"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7z",
|
|
73
|
+
"M14 2v5h6",
|
|
74
|
+
"M8 13h8", "M8 17h8", "M8 9h2",
|
|
75
|
+
],
|
|
76
|
+
upload: ["M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", "M7 10l5-5 5 5", "M12 5v12"],
|
|
77
|
+
download: ["M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", "M7 10l5 5 5-5", "M12 15V3"],
|
|
78
|
+
link: [
|
|
79
|
+
"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71",
|
|
80
|
+
"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71",
|
|
81
|
+
],
|
|
82
|
+
alert: ["M12 9v4", "M12 17h.01", "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"],
|
|
83
|
+
// Preview — the document toolbar's "show me this on the site" action.
|
|
84
|
+
eye: [
|
|
85
|
+
"M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7Z",
|
|
86
|
+
"M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z",
|
|
87
|
+
],
|
|
88
|
+
// "Opens somewhere else" — a preview pointing at an external environment.
|
|
89
|
+
externalLink: ["M15 3h6v6", "M10 14 21 3", "M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5"],
|
|
90
|
+
// Grid / list view toggle for the media browser.
|
|
91
|
+
grid: [
|
|
92
|
+
"M3 3h7v7H3z", "M14 3h7v7h-7z", "M14 14h7v7h-7z", "M3 14h7v7H3z",
|
|
93
|
+
],
|
|
94
|
+
rows: [
|
|
95
|
+
"M3 5h18", "M3 12h18", "M3 19h18",
|
|
96
|
+
],
|
|
97
|
+
// A ring with one quarter missing — meaningless standing still, so only ever
|
|
98
|
+
// rendered inside <Spinner/>, which rotates it.
|
|
99
|
+
loader: ["M21 12a9 9 0 1 1-6.219-8.56"],
|
|
100
|
+
} as const;
|
|
101
|
+
|
|
102
|
+
export type IconName = keyof typeof icons;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Theme + hooks
|
|
2
|
+
export {
|
|
3
|
+
makeTheme,
|
|
4
|
+
lightTheme,
|
|
5
|
+
darkTheme,
|
|
6
|
+
DESKTOP_BREAKPOINT,
|
|
7
|
+
} from "./theme";
|
|
8
|
+
export type { Theme, ThemeMode } from "./theme";
|
|
9
|
+
export {
|
|
10
|
+
ThemeProvider,
|
|
11
|
+
ThemeScope,
|
|
12
|
+
useTheme,
|
|
13
|
+
useThemeMode,
|
|
14
|
+
useResponsive,
|
|
15
|
+
} from "./ThemeProvider";
|
|
16
|
+
export type { ThemeProviderProps } from "./ThemeProvider";
|
|
17
|
+
|
|
18
|
+
// Icons
|
|
19
|
+
export { Icon } from "./components/Icon";
|
|
20
|
+
export type { IconProps } from "./components/Icon";
|
|
21
|
+
export { icons } from "./icons";
|
|
22
|
+
export type { IconName } from "./icons";
|
|
23
|
+
|
|
24
|
+
// Typography
|
|
25
|
+
export { Text } from "./components/Text";
|
|
26
|
+
export type { TextProps } from "./components/Text";
|
|
27
|
+
|
|
28
|
+
// Forms
|
|
29
|
+
export { Button, IconButton } from "./components/Button";
|
|
30
|
+
export type { ButtonProps, ButtonVariant, ButtonSize, IconButtonProps } from "./components/Button";
|
|
31
|
+
export { Input } from "./components/Input";
|
|
32
|
+
export type { InputProps } from "./components/Input";
|
|
33
|
+
|
|
34
|
+
// Data display
|
|
35
|
+
export { Card, Badge, DiffStat, Avatar, Divider, SectionLabel } from "./components/primitives";
|
|
36
|
+
export type { BadgeTone } from "./components/primitives";
|
|
37
|
+
export { NavRow } from "./components/NavRow";
|
|
38
|
+
export type { NavRowProps } from "./components/NavRow";
|
|
39
|
+
|
|
40
|
+
// Surface switching + branch selection (the editor top bar)
|
|
41
|
+
export { Segment } from "./components/Segment";
|
|
42
|
+
export type { SegmentProps, SegmentItem } from "./components/Segment";
|
|
43
|
+
export { BranchMenu } from "./components/BranchMenu";
|
|
44
|
+
export type { BranchMenuProps, BranchRef } from "./components/BranchMenu";
|
|
45
|
+
export { ProjectMenu } from "./components/ProjectMenu";
|
|
46
|
+
export type { ProjectMenuProps, ProjectRef } from "./components/ProjectMenu";
|
|
47
|
+
|
|
48
|
+
// Apply changes (merge) progress modal
|
|
49
|
+
export { ApplyChangesModal } from "./components/ApplyChangesModal";
|
|
50
|
+
export type {
|
|
51
|
+
ApplyChangesModalProps,
|
|
52
|
+
MergeProgress,
|
|
53
|
+
MergeStatus,
|
|
54
|
+
MergeStep,
|
|
55
|
+
} from "./components/ApplyChangesModal";
|
|
56
|
+
|
|
57
|
+
// Change-request summary surface (developer escalation)
|
|
58
|
+
export { ChangeRequestSummary } from "./components/ChangeRequestSummary";
|
|
59
|
+
export type {
|
|
60
|
+
ChangeRequestSummaryProps,
|
|
61
|
+
ChangeRequestSummaryData,
|
|
62
|
+
ChangeRequestComment,
|
|
63
|
+
} from "./components/ChangeRequestSummary";
|
|
64
|
+
|
|
65
|
+
// Changes surface: one document's diff between a branch and its base
|
|
66
|
+
export { ChangeDetail } from "./components/ChangeDetail";
|
|
67
|
+
export type {
|
|
68
|
+
ChangeDetailProps,
|
|
69
|
+
DocumentChange,
|
|
70
|
+
FieldChange,
|
|
71
|
+
FieldChangeKind,
|
|
72
|
+
ChangePreview,
|
|
73
|
+
FieldConflict,
|
|
74
|
+
ConflictChoice,
|
|
75
|
+
} from "./components/ChangeDetail";
|
|
76
|
+
|
|
77
|
+
// Onboarding: the guided-tour framework (spotlight overlay + target registry).
|
|
78
|
+
// The tours themselves are defined by the host app, not here.
|
|
79
|
+
export {
|
|
80
|
+
OnboardingProvider,
|
|
81
|
+
OnboardingOverlay,
|
|
82
|
+
OnboardingTarget,
|
|
83
|
+
useOnboarding,
|
|
84
|
+
useOnboardingOptional,
|
|
85
|
+
useOnboardingTarget,
|
|
86
|
+
placeCard,
|
|
87
|
+
} from "./components/Onboarding";
|
|
88
|
+
export type {
|
|
89
|
+
OnboardingProviderProps,
|
|
90
|
+
OnboardingFlow,
|
|
91
|
+
OnboardingFlowStep,
|
|
92
|
+
OnboardingPlacement,
|
|
93
|
+
OnboardingProgress,
|
|
94
|
+
OnboardingStatus,
|
|
95
|
+
OnboardingSession,
|
|
96
|
+
OnboardingApi,
|
|
97
|
+
} from "./components/Onboarding";
|
|
98
|
+
|
|
99
|
+
// First-import gate: the full-screen states an editor holds while (or instead of)
|
|
100
|
+
// opening a branch that has never been imported.
|
|
101
|
+
export { BranchImportingScreen, BranchImportFailedScreen } from "./components/BranchImport";
|
|
102
|
+
export type {
|
|
103
|
+
BranchImportingScreenProps,
|
|
104
|
+
BranchImportFailedScreenProps,
|
|
105
|
+
} from "./components/BranchImport";
|
|
106
|
+
|
|
107
|
+
// Spinner
|
|
108
|
+
export { Spinner } from "./components/Spinner";
|
|
109
|
+
export type { SpinnerProps } from "./components/Spinner";
|
|
110
|
+
|
|
111
|
+
// Notifications
|
|
112
|
+
export { NotificationBell, NotificationList } from "./components/Notifications";
|
|
113
|
+
export type {
|
|
114
|
+
NotificationItem,
|
|
115
|
+
NotificationBellProps,
|
|
116
|
+
NotificationListProps,
|
|
117
|
+
} from "./components/Notifications";
|
|
118
|
+
|
|
119
|
+
// Layout
|
|
120
|
+
export {
|
|
121
|
+
Screen,
|
|
122
|
+
TopBar,
|
|
123
|
+
Pane,
|
|
124
|
+
AppShell,
|
|
125
|
+
MobileScreen,
|
|
126
|
+
ThemeToggle,
|
|
127
|
+
} from "./components/layout";
|
|
128
|
+
|
|
129
|
+
// Media: the picker control, its previews, and the host data seam
|
|
130
|
+
export { MediaField, MediaPicker, MediaProvider, DocumentPathProvider, useMediaApi, pathForStoreAs } from "./components/MediaField";
|
|
131
|
+
export type { MediaFieldProps, MediaPickerProps } from "./components/MediaField";
|
|
132
|
+
export { MediaPreview, MediaChip } from "./components/MediaPreview";
|
|
133
|
+
export type { MediaPreviewProps } from "./components/MediaPreview";
|
|
134
|
+
export { MediaBrowser } from "./components/MediaBrowser";
|
|
135
|
+
export type { MediaBrowserProps } from "./components/MediaBrowser";
|
|
136
|
+
export {
|
|
137
|
+
MEDIA_KINDS,
|
|
138
|
+
MEDIA_OBJECT_FIELDS,
|
|
139
|
+
isMediaValue,
|
|
140
|
+
kindIcon,
|
|
141
|
+
formatBytes,
|
|
142
|
+
// Nav-key helpers: the sidebar's Media button is derived by ContentBrowser, so
|
|
143
|
+
// a host needs these to map its URL state onto the media surface and the
|
|
144
|
+
// selected set.
|
|
145
|
+
MEDIA_NAV_KEY,
|
|
146
|
+
mediaNavKey,
|
|
147
|
+
isMediaNavKey,
|
|
148
|
+
parseMediaNavKey,
|
|
149
|
+
} from "./media";
|
|
150
|
+
export type {
|
|
151
|
+
MediaKind,
|
|
152
|
+
StoreAs,
|
|
153
|
+
MediaValue,
|
|
154
|
+
MediaAsset,
|
|
155
|
+
MediaResolution,
|
|
156
|
+
MediaSetInfo,
|
|
157
|
+
MediaApi,
|
|
158
|
+
MediaCondition,
|
|
159
|
+
MediaFilterField,
|
|
160
|
+
MediaFilterOp,
|
|
161
|
+
MediaFacetValue,
|
|
162
|
+
MediaFacets,
|
|
163
|
+
} from "./media";
|
|
164
|
+
|
|
165
|
+
// Field components registry (mirror of the Go schema's component set)
|
|
166
|
+
export {
|
|
167
|
+
componentsByType,
|
|
168
|
+
defaultComponentByType,
|
|
169
|
+
defaultComponent,
|
|
170
|
+
resolveComponent,
|
|
171
|
+
componentAllowed,
|
|
172
|
+
} from "./fieldComponents";
|
|
173
|
+
|
|
174
|
+
// Templates
|
|
175
|
+
export { ContentBrowser } from "./components/ContentBrowser";
|
|
176
|
+
export { ContentBrowserSkeleton, SkeletonBox } from "./components/Skeleton";
|
|
177
|
+
export type {
|
|
178
|
+
ContentBrowserProps,
|
|
179
|
+
CmsNavItem,
|
|
180
|
+
CmsNavSection,
|
|
181
|
+
UserMenuItem,
|
|
182
|
+
CmsEntry,
|
|
183
|
+
OpenTab,
|
|
184
|
+
EntryField,
|
|
185
|
+
FacetField,
|
|
186
|
+
DocFilter,
|
|
187
|
+
CmsWorkspace,
|
|
188
|
+
FieldSlotArgs,
|
|
189
|
+
RenderField,
|
|
190
|
+
EntrySaveChange,
|
|
191
|
+
SaveEntry,
|
|
192
|
+
SaveStatus,
|
|
193
|
+
CollabApi,
|
|
194
|
+
CollabText,
|
|
195
|
+
CollabRegister,
|
|
196
|
+
CollabPeer,
|
|
197
|
+
CollabParticipant,
|
|
198
|
+
} from "./components/ContentBrowser";
|
package/src/media.ts
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// The media picker's display keys and its render classification.
|
|
2
|
+
//
|
|
3
|
+
// This is the TypeScript mirror of packages/importer/schema/media.go — the Go
|
|
4
|
+
// file is the source of truth validated on config import; keep the two in sync.
|
|
5
|
+
|
|
6
|
+
import type { IconName } from "./icons";
|
|
7
|
+
|
|
8
|
+
// Media is one asset as the server delivers it. `kind` is derived server-side
|
|
9
|
+
// from the mime type, so a preview never has to guess from a filename; `url` is
|
|
10
|
+
// a short-lived presigned read URL (the object store itself is never public).
|
|
11
|
+
export interface MediaAsset {
|
|
12
|
+
id: string;
|
|
13
|
+
set: string;
|
|
14
|
+
repoPath: string;
|
|
15
|
+
publicPath?: string | null;
|
|
16
|
+
cdnPath?: string | null;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
// Lower-cased, no dot ("png"). Empty for a file without one.
|
|
19
|
+
extension: string;
|
|
20
|
+
kind: MediaKind;
|
|
21
|
+
size: number;
|
|
22
|
+
width?: number | null;
|
|
23
|
+
height?: number | null;
|
|
24
|
+
pending: boolean;
|
|
25
|
+
url: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// MediaResolution is the answer to "what asset does this frontmatter string
|
|
29
|
+
// mean?". `media` null with `ambiguous` false is a miss, not an error — the
|
|
30
|
+
// string is an external URL or points outside every configured set, and the
|
|
31
|
+
// field should render it as-is.
|
|
32
|
+
export interface MediaResolution {
|
|
33
|
+
input: string;
|
|
34
|
+
media?: MediaAsset | null;
|
|
35
|
+
aliasKind?: string | null;
|
|
36
|
+
ambiguous: boolean;
|
|
37
|
+
candidates: MediaAsset[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// MediaSetInfo is one browsable group of assets, from the repository's config.
|
|
41
|
+
export interface MediaSetInfo {
|
|
42
|
+
name: string;
|
|
43
|
+
path: string;
|
|
44
|
+
mimeTypes: string[];
|
|
45
|
+
publicBase?: string | null;
|
|
46
|
+
cdnBase?: string | null;
|
|
47
|
+
count: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// One condition in the media browser's faceted search. The field set is closed
|
|
51
|
+
// and enforced server-side; an unknown field or a mismatched operator is an
|
|
52
|
+
// error rather than a dropped filter.
|
|
53
|
+
//
|
|
54
|
+
// name repository path, case-insensitive substring — contains, eq
|
|
55
|
+
// extension lower-cased, no dot ("png") — eq, in
|
|
56
|
+
// mimeType exact ("image/png") or wildcard ("image/*") — eq, in
|
|
57
|
+
// kind image|svg|video|audio|pdf|file — eq, in
|
|
58
|
+
// size bytes — eq, gt, lt, gte, lte
|
|
59
|
+
// width pixels — eq, gt, lt, gte, lte
|
|
60
|
+
// height pixels — eq, gt, lt, gte, lte
|
|
61
|
+
// pending "true"/"false" — eq
|
|
62
|
+
export type MediaFilterField =
|
|
63
|
+
| "name" | "extension" | "mimeType" | "kind"
|
|
64
|
+
| "size" | "width" | "height" | "pending";
|
|
65
|
+
|
|
66
|
+
export type MediaFilterOp = "eq" | "in" | "contains" | "gt" | "lt" | "gte" | "lte";
|
|
67
|
+
|
|
68
|
+
export interface MediaCondition {
|
|
69
|
+
field: MediaFilterField;
|
|
70
|
+
op: MediaFilterOp;
|
|
71
|
+
value?: string;
|
|
72
|
+
values?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// One available value for a facet, with how many files carry it — so the panel
|
|
76
|
+
// can show "png (42)" and omit options that would return nothing.
|
|
77
|
+
export interface MediaFacetValue {
|
|
78
|
+
value: string;
|
|
79
|
+
count: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// The filter panel's options for a set. Counts deliberately ignore the user's
|
|
83
|
+
// own selections, so the options don't vanish from under them as they click.
|
|
84
|
+
export interface MediaFacets {
|
|
85
|
+
extensions: MediaFacetValue[];
|
|
86
|
+
mimeTypes: MediaFacetValue[];
|
|
87
|
+
kinds: MediaFacetValue[];
|
|
88
|
+
minSize: number;
|
|
89
|
+
maxSize: number;
|
|
90
|
+
total: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Media occupies one nav key space, prefixed so it cannot collide with a
|
|
94
|
+
// collection: a collection name is restricted to [A-Za-z0-9_-] by the config
|
|
95
|
+
// schema, so it can never contain a colon — not even a collection literally
|
|
96
|
+
// named "media".
|
|
97
|
+
//
|
|
98
|
+
// "media:" the Media button, with no set chosen yet
|
|
99
|
+
// "media:uploads" a specific set, which is what makes a set deep-linkable
|
|
100
|
+
const MEDIA_NAV_PREFIX = "media:";
|
|
101
|
+
|
|
102
|
+
// MEDIA_NAV_KEY is the Media button's own key — media with nothing selected.
|
|
103
|
+
export const MEDIA_NAV_KEY = MEDIA_NAV_PREFIX;
|
|
104
|
+
|
|
105
|
+
// mediaNavKey returns the nav key for a set, or for the Media button when the
|
|
106
|
+
// set is omitted.
|
|
107
|
+
export function mediaNavKey(set?: string): string {
|
|
108
|
+
return set ? MEDIA_NAV_PREFIX + set : MEDIA_NAV_PREFIX;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// isMediaNavKey reports whether a nav key addresses media at all — true for the
|
|
112
|
+
// Media button and for any set beneath it. This is what a host checks to know
|
|
113
|
+
// the media surface is showing.
|
|
114
|
+
export function isMediaNavKey(key: string | null | undefined): boolean {
|
|
115
|
+
return !!key && key.startsWith(MEDIA_NAV_PREFIX);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// parseMediaNavKey returns the media set a nav key selects: null for an ordinary
|
|
119
|
+
// collection, and also null for the bare Media button (media is showing, but no
|
|
120
|
+
// set is chosen). Pair it with isMediaNavKey to tell those two apart.
|
|
121
|
+
export function parseMediaNavKey(key: string | null | undefined): string | null {
|
|
122
|
+
if (!isMediaNavKey(key)) return null;
|
|
123
|
+
const set = key!.slice(MEDIA_NAV_PREFIX.length);
|
|
124
|
+
return set.length > 0 ? set : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MediaApi is the data seam between the design system and its host. The DS owns
|
|
128
|
+
// the picker and preview UI; the host (Apollo on web/mobile, REST on desktop)
|
|
129
|
+
// owns fetching. Omitting it from ContentBrowser renders media fields read-only,
|
|
130
|
+
// which is also what a deployment with no object store gets.
|
|
131
|
+
export interface MediaApi {
|
|
132
|
+
// The sets available to browse.
|
|
133
|
+
sets: MediaSetInfo[];
|
|
134
|
+
// Resolve frontmatter strings to assets. Batched by the caller; the server
|
|
135
|
+
// answers all of a document's media fields in one round trip.
|
|
136
|
+
resolve: (paths: string[], documentPath?: string) => Promise<MediaResolution[]>;
|
|
137
|
+
// Fetch one asset by id — the object value form's lookup, which needs no
|
|
138
|
+
// resolution because the id is authoritative. Null when the asset is gone.
|
|
139
|
+
byId: (id: string) => Promise<MediaAsset | null>;
|
|
140
|
+
// One page of a set's assets, for both the field picker and the browser.
|
|
141
|
+
list: (params: {
|
|
142
|
+
set?: string;
|
|
143
|
+
kinds?: MediaKind[];
|
|
144
|
+
search?: string;
|
|
145
|
+
filters?: MediaCondition[];
|
|
146
|
+
limit?: number;
|
|
147
|
+
offset?: number;
|
|
148
|
+
}) => Promise<MediaAsset[]>;
|
|
149
|
+
// How many assets match, ignoring paging — the total behind "12 of 340" and
|
|
150
|
+
// the count shown while building filters. Omit to hide those affordances.
|
|
151
|
+
count?: (params: {
|
|
152
|
+
set?: string;
|
|
153
|
+
kinds?: MediaKind[];
|
|
154
|
+
search?: string;
|
|
155
|
+
filters?: MediaCondition[];
|
|
156
|
+
}) => Promise<number>;
|
|
157
|
+
// The filter panel's options for a set. Omit to render the browser without a
|
|
158
|
+
// faceted panel (plain search only).
|
|
159
|
+
facets?: (set?: string) => Promise<MediaFacets>;
|
|
160
|
+
// Upload a file into a set. Resolves to the finalized asset. Omit to disable
|
|
161
|
+
// the picker's upload affordance.
|
|
162
|
+
upload?: (params: {
|
|
163
|
+
set: string;
|
|
164
|
+
file: File;
|
|
165
|
+
folder?: string;
|
|
166
|
+
}) => Promise<MediaAsset>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// MediaKind is the render class a preview switches on. It is derived server-side
|
|
170
|
+
// on import and delivered on every Media object, so clients never re-derive it
|
|
171
|
+
// from a filename (and never disagree with the server about it).
|
|
172
|
+
export type MediaKind = "image" | "svg" | "video" | "audio" | "pdf" | "file";
|
|
173
|
+
|
|
174
|
+
export const MEDIA_KINDS: MediaKind[] = ["image", "svg", "video", "audio", "pdf", "file"];
|
|
175
|
+
|
|
176
|
+
// StoreAs is which form of the path a string media field writes when the user
|
|
177
|
+
// picks an asset. The object form needs no equivalent: it stores the media id.
|
|
178
|
+
export type StoreAs = "repo" | "public" | "cdn";
|
|
179
|
+
|
|
180
|
+
// The child fields an object media field must have, in order.
|
|
181
|
+
export const MEDIA_OBJECT_FIELDS = ["id", "public_path", "cdn_path"] as const;
|
|
182
|
+
|
|
183
|
+
// MediaValue is the object form of a media field's value.
|
|
184
|
+
export interface MediaValue {
|
|
185
|
+
id: string;
|
|
186
|
+
public_path?: string | null;
|
|
187
|
+
cdn_path?: string | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// isMediaValue narrows an unknown field value to the object form. A string media
|
|
191
|
+
// field's value is a plain string and needs resolution to find its asset; the
|
|
192
|
+
// object form carries the id and does not.
|
|
193
|
+
export function isMediaValue(v: unknown): v is MediaValue {
|
|
194
|
+
return typeof v === "object" && v !== null && typeof (v as MediaValue).id === "string";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// kindIcon maps a render kind to the design-system icon used for the collapsed /
|
|
198
|
+
// non-previewable representation of an asset.
|
|
199
|
+
export function kindIcon(kind: MediaKind): IconName {
|
|
200
|
+
switch (kind) {
|
|
201
|
+
case "image":
|
|
202
|
+
case "svg":
|
|
203
|
+
return "image";
|
|
204
|
+
case "video":
|
|
205
|
+
return "video";
|
|
206
|
+
case "audio":
|
|
207
|
+
return "audio";
|
|
208
|
+
case "pdf":
|
|
209
|
+
return "fileText";
|
|
210
|
+
default:
|
|
211
|
+
return "file";
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// formatBytes renders a file size for display next to an asset.
|
|
216
|
+
export function formatBytes(bytes: number): string {
|
|
217
|
+
if (!Number.isFinite(bytes) || bytes < 0) return "";
|
|
218
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
219
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
220
|
+
let value = bytes / 1024;
|
|
221
|
+
let unit = 0;
|
|
222
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
223
|
+
value /= 1024;
|
|
224
|
+
unit++;
|
|
225
|
+
}
|
|
226
|
+
// One decimal below 10 ("1.4 MB"), none above ("14 MB") — enough precision to
|
|
227
|
+
// distinguish sizes without implying more than the number is worth.
|
|
228
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
229
|
+
}
|