@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.
Files changed (70) hide show
  1. package/README.md +77 -0
  2. package/css/components.css +312 -0
  3. package/css/tokens.css +212 -0
  4. package/package.json +65 -0
  5. package/src/ThemeProvider.tsx +86 -0
  6. package/src/__tests__/ApplyChangesModal.test.tsx +148 -0
  7. package/src/__tests__/BranchImport.test.tsx +46 -0
  8. package/src/__tests__/Button.test.tsx +45 -0
  9. package/src/__tests__/ChangeRequestSummary.test.tsx +57 -0
  10. package/src/__tests__/ContentBrowser.changes.test.tsx +611 -0
  11. package/src/__tests__/ContentBrowser.collab.test.tsx +322 -0
  12. package/src/__tests__/ContentBrowser.collabsync.test.tsx +264 -0
  13. package/src/__tests__/ContentBrowser.contentslot.test.tsx +53 -0
  14. package/src/__tests__/ContentBrowser.discriminator.test.tsx +142 -0
  15. package/src/__tests__/ContentBrowser.drafts.test.tsx +271 -0
  16. package/src/__tests__/ContentBrowser.fields.test.tsx +117 -0
  17. package/src/__tests__/ContentBrowser.media.test.tsx +140 -0
  18. package/src/__tests__/ContentBrowser.mixedcollab.test.tsx +63 -0
  19. package/src/__tests__/ContentBrowser.mixedvalues.test.tsx +38 -0
  20. package/src/__tests__/ContentBrowser.pagination.test.tsx +62 -0
  21. package/src/__tests__/ContentBrowser.previewtab.test.tsx +212 -0
  22. package/src/__tests__/ContentBrowser.reorder.test.tsx +45 -0
  23. package/src/__tests__/ContentBrowser.search.test.tsx +135 -0
  24. package/src/__tests__/ContentBrowser.selectvalue.test.tsx +185 -0
  25. package/src/__tests__/ContentBrowser.staged.test.tsx +132 -0
  26. package/src/__tests__/ContentBrowser.usermenu.test.tsx +56 -0
  27. package/src/__tests__/MediaBrowser.test.tsx +353 -0
  28. package/src/__tests__/MediaField.test.tsx +185 -0
  29. package/src/__tests__/Notifications.test.tsx +69 -0
  30. package/src/__tests__/Onboarding.test.tsx +287 -0
  31. package/src/__tests__/cssTokens.test.ts +201 -0
  32. package/src/__tests__/fieldComponents.test.ts +43 -0
  33. package/src/__tests__/reorder.test.ts +58 -0
  34. package/src/components/ApplyChangesModal.tsx +348 -0
  35. package/src/components/BranchImport.tsx +157 -0
  36. package/src/components/BranchMenu.tsx +192 -0
  37. package/src/components/Button.tsx +131 -0
  38. package/src/components/ChangeDetail.tsx +472 -0
  39. package/src/components/ChangeRequestSummary.tsx +173 -0
  40. package/src/components/CollabField.tsx +388 -0
  41. package/src/components/ContentBrowser.tsx +5073 -0
  42. package/src/components/Icon.tsx +28 -0
  43. package/src/components/Icon.web.tsx +31 -0
  44. package/src/components/Input.tsx +106 -0
  45. package/src/components/MediaBrowser.tsx +766 -0
  46. package/src/components/MediaField.tsx +670 -0
  47. package/src/components/MediaPreview.tsx +91 -0
  48. package/src/components/MediaPreview.web.tsx +169 -0
  49. package/src/components/NavRow.tsx +105 -0
  50. package/src/components/Notifications.tsx +301 -0
  51. package/src/components/Onboarding.tsx +751 -0
  52. package/src/components/ProjectMenu.tsx +124 -0
  53. package/src/components/Segment.tsx +87 -0
  54. package/src/components/Skeleton.tsx +216 -0
  55. package/src/components/Spinner.tsx +44 -0
  56. package/src/components/Text.tsx +85 -0
  57. package/src/components/documentDrafts.ts +213 -0
  58. package/src/components/layout.tsx +284 -0
  59. package/src/components/primitives.tsx +143 -0
  60. package/src/components/reorder.ts +40 -0
  61. package/src/fieldComponents.ts +63 -0
  62. package/src/icons.ts +102 -0
  63. package/src/index.ts +198 -0
  64. package/src/media.ts +229 -0
  65. package/src/theme.ts +116 -0
  66. package/src/web/Button.tsx +110 -0
  67. package/src/web/Icon.tsx +52 -0
  68. package/src/web/Input.tsx +39 -0
  69. package/src/web/index.ts +43 -0
  70. 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
+ }