@cratis/scene.blueprint.default 4.40.0 → 4.42.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 (251) hide show
  1. package/ComponentName.ts +9 -0
  2. package/defaultBlueprint.ts +1 -0
  3. package/defaultBlueprintComponents.ts +4 -1
  4. package/dist/cjs/ComponentName.js +3 -0
  5. package/dist/cjs/ComponentName.js.map +1 -1
  6. package/dist/cjs/defaultBlueprint.js +20 -1
  7. package/dist/cjs/defaultBlueprint.js.map +1 -1
  8. package/dist/cjs/defaultBlueprintComponentMetadata.js +11 -0
  9. package/dist/cjs/defaultBlueprintComponentMetadata.js.map +1 -1
  10. package/dist/cjs/defaultBlueprintComponents.js +17 -0
  11. package/dist/cjs/defaultBlueprintComponents.js.map +1 -1
  12. package/dist/cjs/foundation/ActionButton.js.map +1 -1
  13. package/dist/cjs/foundation/ActionButtonControl.js +46 -0
  14. package/dist/cjs/foundation/ActionButtonControl.js.map +1 -0
  15. package/dist/cjs/foundation/DialogFrame.js +196 -0
  16. package/dist/cjs/foundation/DialogFrame.js.map +1 -0
  17. package/dist/cjs/foundation/FormScreenBody.js +13 -3
  18. package/dist/cjs/foundation/FormScreenBody.js.map +1 -1
  19. package/dist/cjs/foundation/Notice.js +51 -0
  20. package/dist/cjs/foundation/Notice.js.map +1 -0
  21. package/dist/cjs/foundation/formModel.js +1 -0
  22. package/dist/cjs/foundation/formModel.js.map +1 -1
  23. package/dist/cjs/foundation/foundationComponentMetadata.js +22 -0
  24. package/dist/cjs/foundation/foundationComponentMetadata.js.map +1 -1
  25. package/dist/cjs/foundation/foundationDescriptors.js +89 -0
  26. package/dist/cjs/foundation/foundationDescriptors.js.map +1 -1
  27. package/dist/cjs/foundation/foundationSamples.js +29 -0
  28. package/dist/cjs/foundation/foundationSamples.js.map +1 -1
  29. package/dist/cjs/foundation/foundationStrings.js +3 -1
  30. package/dist/cjs/foundation/foundationStrings.js.map +1 -1
  31. package/dist/cjs/foundation/templates/boardScreenTemplate.js +76 -0
  32. package/dist/cjs/foundation/templates/boardScreenTemplate.js.map +1 -0
  33. package/dist/cjs/foundation/templates/dashboardScreenTemplate.js +128 -0
  34. package/dist/cjs/foundation/templates/dashboardScreenTemplate.js.map +1 -0
  35. package/dist/cjs/foundation/templates/dialogTemplates.js +254 -0
  36. package/dist/cjs/foundation/templates/dialogTemplates.js.map +1 -0
  37. package/dist/cjs/foundation/templates/formScreenTemplate.js +71 -0
  38. package/dist/cjs/foundation/templates/formScreenTemplate.js.map +1 -0
  39. package/dist/cjs/foundation/templates/index.js +57 -0
  40. package/dist/cjs/foundation/templates/index.js.map +1 -0
  41. package/dist/cjs/foundation/templates/listScreenTemplate.js +124 -0
  42. package/dist/cjs/foundation/templates/listScreenTemplate.js.map +1 -0
  43. package/dist/cjs/foundation/templates/masterDetailScreenTemplate.js +79 -0
  44. package/dist/cjs/foundation/templates/masterDetailScreenTemplate.js.map +1 -0
  45. package/dist/cjs/foundation/templates/recordPageTemplate.js +137 -0
  46. package/dist/cjs/foundation/templates/recordPageTemplate.js.map +1 -0
  47. package/dist/cjs/foundation/templates/stateScreenTemplates.js +252 -0
  48. package/dist/cjs/foundation/templates/stateScreenTemplates.js.map +1 -0
  49. package/dist/cjs/foundation/templates/templateParts.js +38 -0
  50. package/dist/cjs/foundation/templates/templateParts.js.map +1 -0
  51. package/dist/cjs/foundation/templates/wizardScreenTemplate.js +95 -0
  52. package/dist/cjs/foundation/templates/wizardScreenTemplate.js.map +1 -0
  53. package/dist/cjs/foundation/templates/workspaceScreenTemplate.js +62 -0
  54. package/dist/cjs/foundation/templates/workspaceScreenTemplate.js.map +1 -0
  55. package/dist/cjs/gallery/GalleryDialogPreview.js +62 -0
  56. package/dist/cjs/gallery/GalleryDialogPreview.js.map +1 -0
  57. package/dist/cjs/gallery/GalleryScreenPreview.js +78 -4
  58. package/dist/cjs/gallery/GalleryScreenPreview.js.map +1 -1
  59. package/dist/cjs/gallery/applicationChrome.js +18 -1
  60. package/dist/cjs/gallery/applicationChrome.js.map +1 -1
  61. package/dist/cjs/gallery/composeScreen.js +5 -3
  62. package/dist/cjs/gallery/composeScreen.js.map +1 -1
  63. package/dist/cjs/gallery/dialogTemplates.js +41 -36
  64. package/dist/cjs/gallery/dialogTemplates.js.map +1 -1
  65. package/dist/cjs/gallery/screens.js +87 -3
  66. package/dist/cjs/gallery/screens.js.map +1 -1
  67. package/dist/cjs/index.js +61 -5
  68. package/dist/cjs/index.js.map +1 -1
  69. package/dist/esm/ComponentName.d.ts +3 -0
  70. package/dist/esm/ComponentName.d.ts.map +1 -1
  71. package/dist/esm/ComponentName.js +3 -0
  72. package/dist/esm/ComponentName.js.map +1 -1
  73. package/dist/esm/defaultBlueprint.d.ts.map +1 -1
  74. package/dist/esm/defaultBlueprint.js +20 -1
  75. package/dist/esm/defaultBlueprint.js.map +1 -1
  76. package/dist/esm/defaultBlueprintComponentMetadata.js +11 -0
  77. package/dist/esm/defaultBlueprintComponentMetadata.js.map +1 -1
  78. package/dist/esm/defaultBlueprintComponents.d.ts.map +1 -1
  79. package/dist/esm/defaultBlueprintComponents.js +17 -0
  80. package/dist/esm/defaultBlueprintComponents.js.map +1 -1
  81. package/dist/esm/foundation/ActionButton.d.ts +1 -1
  82. package/dist/esm/foundation/ActionButton.d.ts.map +1 -1
  83. package/dist/esm/foundation/ActionButton.js.map +1 -1
  84. package/dist/esm/foundation/ActionButtonControl.d.ts +3 -0
  85. package/dist/esm/foundation/ActionButtonControl.d.ts.map +1 -0
  86. package/dist/esm/foundation/ActionButtonControl.js +44 -0
  87. package/dist/esm/foundation/ActionButtonControl.js.map +1 -0
  88. package/dist/esm/foundation/DialogFrame.d.ts +14 -0
  89. package/dist/esm/foundation/DialogFrame.d.ts.map +1 -0
  90. package/dist/esm/foundation/DialogFrame.js +190 -0
  91. package/dist/esm/foundation/DialogFrame.js.map +1 -0
  92. package/dist/esm/foundation/FormScreenBody.d.ts.map +1 -1
  93. package/dist/esm/foundation/FormScreenBody.js +13 -3
  94. package/dist/esm/foundation/FormScreenBody.js.map +1 -1
  95. package/dist/esm/foundation/Notice.d.ts +3 -0
  96. package/dist/esm/foundation/Notice.d.ts.map +1 -0
  97. package/dist/esm/foundation/Notice.js +49 -0
  98. package/dist/esm/foundation/Notice.js.map +1 -0
  99. package/dist/esm/foundation/formModel.d.ts +2 -1
  100. package/dist/esm/foundation/formModel.d.ts.map +1 -1
  101. package/dist/esm/foundation/formModel.js +1 -0
  102. package/dist/esm/foundation/formModel.js.map +1 -1
  103. package/dist/esm/foundation/foundationComponentMetadata.d.ts.map +1 -1
  104. package/dist/esm/foundation/foundationComponentMetadata.js +22 -0
  105. package/dist/esm/foundation/foundationComponentMetadata.js.map +1 -1
  106. package/dist/esm/foundation/foundationDescriptors.d.ts.map +1 -1
  107. package/dist/esm/foundation/foundationDescriptors.js +89 -0
  108. package/dist/esm/foundation/foundationDescriptors.js.map +1 -1
  109. package/dist/esm/foundation/foundationSamples.d.ts.map +1 -1
  110. package/dist/esm/foundation/foundationSamples.js +29 -0
  111. package/dist/esm/foundation/foundationSamples.js.map +1 -1
  112. package/dist/esm/foundation/foundationStrings.d.ts +2 -0
  113. package/dist/esm/foundation/foundationStrings.d.ts.map +1 -1
  114. package/dist/esm/foundation/foundationStrings.js +3 -1
  115. package/dist/esm/foundation/foundationStrings.js.map +1 -1
  116. package/dist/esm/foundation/index.d.ts +4 -0
  117. package/dist/esm/foundation/index.d.ts.map +1 -1
  118. package/dist/esm/foundation/index.js +4 -0
  119. package/dist/esm/foundation/index.js.map +1 -1
  120. package/dist/esm/foundation/templates/boardScreenTemplate.d.ts +3 -0
  121. package/dist/esm/foundation/templates/boardScreenTemplate.d.ts.map +1 -0
  122. package/dist/esm/foundation/templates/boardScreenTemplate.js +74 -0
  123. package/dist/esm/foundation/templates/boardScreenTemplate.js.map +1 -0
  124. package/dist/esm/foundation/templates/dashboardScreenTemplate.d.ts +3 -0
  125. package/dist/esm/foundation/templates/dashboardScreenTemplate.d.ts.map +1 -0
  126. package/dist/esm/foundation/templates/dashboardScreenTemplate.js +126 -0
  127. package/dist/esm/foundation/templates/dashboardScreenTemplate.js.map +1 -0
  128. package/dist/esm/foundation/templates/dialogTemplates.d.ts +20 -0
  129. package/dist/esm/foundation/templates/dialogTemplates.d.ts.map +1 -0
  130. package/dist/esm/foundation/templates/dialogTemplates.js +243 -0
  131. package/dist/esm/foundation/templates/dialogTemplates.js.map +1 -0
  132. package/dist/esm/foundation/templates/formScreenTemplate.d.ts +3 -0
  133. package/dist/esm/foundation/templates/formScreenTemplate.d.ts.map +1 -0
  134. package/dist/esm/foundation/templates/formScreenTemplate.js +69 -0
  135. package/dist/esm/foundation/templates/formScreenTemplate.js.map +1 -0
  136. package/dist/esm/foundation/templates/index.d.ts +13 -0
  137. package/dist/esm/foundation/templates/index.d.ts.map +1 -0
  138. package/dist/esm/foundation/templates/index.js +27 -0
  139. package/dist/esm/foundation/templates/index.js.map +1 -0
  140. package/dist/esm/foundation/templates/listScreenTemplate.d.ts +3 -0
  141. package/dist/esm/foundation/templates/listScreenTemplate.d.ts.map +1 -0
  142. package/dist/esm/foundation/templates/listScreenTemplate.js +122 -0
  143. package/dist/esm/foundation/templates/listScreenTemplate.js.map +1 -0
  144. package/dist/esm/foundation/templates/masterDetailScreenTemplate.d.ts +3 -0
  145. package/dist/esm/foundation/templates/masterDetailScreenTemplate.d.ts.map +1 -0
  146. package/dist/esm/foundation/templates/masterDetailScreenTemplate.js +77 -0
  147. package/dist/esm/foundation/templates/masterDetailScreenTemplate.js.map +1 -0
  148. package/dist/esm/foundation/templates/recordPageTemplate.d.ts +3 -0
  149. package/dist/esm/foundation/templates/recordPageTemplate.d.ts.map +1 -0
  150. package/dist/esm/foundation/templates/recordPageTemplate.js +135 -0
  151. package/dist/esm/foundation/templates/recordPageTemplate.js.map +1 -0
  152. package/dist/esm/foundation/templates/stateScreenTemplates.d.ts +8 -0
  153. package/dist/esm/foundation/templates/stateScreenTemplates.d.ts.map +1 -0
  154. package/dist/esm/foundation/templates/stateScreenTemplates.js +245 -0
  155. package/dist/esm/foundation/templates/stateScreenTemplates.js.map +1 -0
  156. package/dist/esm/foundation/templates/templateParts.d.ts +6 -0
  157. package/dist/esm/foundation/templates/templateParts.d.ts.map +1 -0
  158. package/dist/esm/foundation/templates/templateParts.js +33 -0
  159. package/dist/esm/foundation/templates/templateParts.js.map +1 -0
  160. package/dist/esm/foundation/templates/wizardScreenTemplate.d.ts +3 -0
  161. package/dist/esm/foundation/templates/wizardScreenTemplate.d.ts.map +1 -0
  162. package/dist/esm/foundation/templates/wizardScreenTemplate.js +93 -0
  163. package/dist/esm/foundation/templates/wizardScreenTemplate.js.map +1 -0
  164. package/dist/esm/foundation/templates/workspaceScreenTemplate.d.ts +3 -0
  165. package/dist/esm/foundation/templates/workspaceScreenTemplate.d.ts.map +1 -0
  166. package/dist/esm/foundation/templates/workspaceScreenTemplate.js +60 -0
  167. package/dist/esm/foundation/templates/workspaceScreenTemplate.js.map +1 -0
  168. package/dist/esm/gallery/FoundationDialogs.stories.d.ts +35 -0
  169. package/dist/esm/gallery/FoundationDialogs.stories.d.ts.map +1 -0
  170. package/dist/esm/gallery/FoundationDialogs.stories.js +38 -0
  171. package/dist/esm/gallery/FoundationDialogs.stories.js.map +1 -0
  172. package/dist/esm/gallery/GalleryDialogPreview.d.ts +17 -0
  173. package/dist/esm/gallery/GalleryDialogPreview.d.ts.map +1 -0
  174. package/dist/esm/gallery/GalleryDialogPreview.js +59 -0
  175. package/dist/esm/gallery/GalleryDialogPreview.js.map +1 -0
  176. package/dist/esm/gallery/GalleryScreenPreview.d.ts +13 -2
  177. package/dist/esm/gallery/GalleryScreenPreview.d.ts.map +1 -1
  178. package/dist/esm/gallery/GalleryScreenPreview.js +77 -5
  179. package/dist/esm/gallery/GalleryScreenPreview.js.map +1 -1
  180. package/dist/esm/gallery/applicationChrome.d.ts.map +1 -1
  181. package/dist/esm/gallery/applicationChrome.js +18 -1
  182. package/dist/esm/gallery/applicationChrome.js.map +1 -1
  183. package/dist/esm/gallery/composeScreen.d.ts.map +1 -1
  184. package/dist/esm/gallery/composeScreen.js +5 -3
  185. package/dist/esm/gallery/composeScreen.js.map +1 -1
  186. package/dist/esm/gallery/dialogTemplates.d.ts.map +1 -1
  187. package/dist/esm/gallery/dialogTemplates.js +42 -37
  188. package/dist/esm/gallery/dialogTemplates.js.map +1 -1
  189. package/dist/esm/gallery/index.d.ts +1 -0
  190. package/dist/esm/gallery/index.d.ts.map +1 -1
  191. package/dist/esm/gallery/index.js +1 -0
  192. package/dist/esm/gallery/index.js.map +1 -1
  193. package/dist/esm/gallery/screens.d.ts.map +1 -1
  194. package/dist/esm/gallery/screens.js +87 -3
  195. package/dist/esm/gallery/screens.js.map +1 -1
  196. package/dist/esm/index.js +17 -1
  197. package/dist/esm/index.js.map +1 -1
  198. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  199. package/foundation/ActionButton.tsx +1 -1
  200. package/foundation/ActionButtonControl.tsx +37 -0
  201. package/foundation/DialogFrame.tsx +138 -0
  202. package/foundation/FormScreenBody.tsx +6 -3
  203. package/foundation/Notice.tsx +33 -0
  204. package/foundation/formModel.ts +1 -0
  205. package/foundation/foundationComponentMetadata.ts +3 -0
  206. package/foundation/foundationDescriptors.ts +41 -0
  207. package/foundation/foundationSamples.ts +3 -0
  208. package/foundation/foundationStrings.ts +2 -0
  209. package/foundation/index.ts +4 -0
  210. package/foundation/templates/boardScreenTemplate.ts +28 -0
  211. package/foundation/templates/dashboardScreenTemplate.ts +37 -0
  212. package/foundation/templates/dialogTemplates.ts +114 -0
  213. package/foundation/templates/formScreenTemplate.ts +28 -0
  214. package/foundation/templates/index.ts +28 -0
  215. package/foundation/templates/listScreenTemplate.ts +37 -0
  216. package/foundation/templates/masterDetailScreenTemplate.ts +28 -0
  217. package/foundation/templates/recordPageTemplate.ts +36 -0
  218. package/foundation/templates/stateScreenTemplates.ts +107 -0
  219. package/foundation/templates/templateParts.ts +30 -0
  220. package/foundation/templates/wizardScreenTemplate.ts +33 -0
  221. package/foundation/templates/workspaceScreenTemplate.ts +27 -0
  222. package/gallery/FoundationDialogs.stories.tsx +74 -0
  223. package/gallery/GalleryDialogPreview.tsx +68 -0
  224. package/gallery/GalleryScreenPreview.tsx +64 -5
  225. package/gallery/applicationChrome.ts +6 -1
  226. package/gallery/composeScreen.ts +4 -2
  227. package/gallery/dialogTemplates.ts +22 -17
  228. package/gallery/index.ts +1 -0
  229. package/gallery/screens.ts +18 -2
  230. package/package.json +10 -8
  231. package/sampleData/foundation-en.json +661 -0
  232. package/shell/layout.css +219 -0
  233. package/thumbnails/BoardScreen.svg +15 -0
  234. package/thumbnails/ConfirmDialog.svg +12 -0
  235. package/thumbnails/DashboardScreen.svg +18 -0
  236. package/thumbnails/DetailDialog.svg +11 -0
  237. package/thumbnails/ErrorScreen.svg +11 -0
  238. package/thumbnails/FilterDialog.svg +16 -0
  239. package/thumbnails/ForbiddenScreen.svg +10 -0
  240. package/thumbnails/FormDialog.svg +12 -0
  241. package/thumbnails/FormScreen.svg +14 -0
  242. package/thumbnails/ListScreen.svg +16 -0
  243. package/thumbnails/LoginScreen.svg +12 -0
  244. package/thumbnails/LookupDialog.svg +13 -0
  245. package/thumbnails/MasterDetailScreen.svg +22 -0
  246. package/thumbnails/NotFoundScreen.svg +9 -0
  247. package/thumbnails/QuickCreateDialog.svg +12 -0
  248. package/thumbnails/RecordPage.svg +17 -0
  249. package/thumbnails/SideSheet.svg +13 -0
  250. package/thumbnails/WizardScreen.svg +16 -0
  251. package/thumbnails/WorkspaceScreen.svg +12 -0
@@ -15,7 +15,7 @@ export interface ActionButtonProps {
15
15
  onClick?: (event: { stopPropagation(): void }) => void;
16
16
 
17
17
  /** The emphasis of the button. */
18
- emphasis?: 'primary' | 'secondary' | 'quiet';
18
+ emphasis?: 'primary' | 'secondary' | 'quiet' | 'danger';
19
19
 
20
20
  children: ReactNode;
21
21
  }
@@ -0,0 +1,37 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { RegisteredComponentProps } from '@cratis/scene.react';
5
+ import { readOptionalString } from '../shell/elementProperties';
6
+ import { ActionButton } from './ActionButton';
7
+ import { readFoundationStrings } from './foundationStrings';
8
+ import { readPermissionRule } from './PermissionRule';
9
+
10
+ const emphases = ['primary', 'secondary', 'quiet', 'danger'] as const;
11
+
12
+ /**
13
+ * The `actionButton` component: one action of a screen or a dialog, drawn the way every action of the foundation is.
14
+ *
15
+ * `label` is what it does - a verb, never "OK" - and `emphasis` is `primary`, `secondary`, `quiet` or `danger` (the confirming button of a destructive
16
+ * dialog). It honours the permission rule of spec Part B 1.5 through `permission`: enabled, disabled (shown, refusing, and saying why) or not rendered.
17
+ * Pressing it runs the behaviors attached to the element, which is how a document makes it close a dialog, navigate or execute a command; with
18
+ * `targetScreen` and no behavior it goes to that screen, as the shell's links do.
19
+ */
20
+ export function ActionButtonControl({ element, interactions }: RegisteredComponentProps) {
21
+ const strings = readFoundationStrings(element);
22
+ const requested = readOptionalString(element, 'emphasis');
23
+ const emphasis = (emphases as readonly string[]).includes(requested ?? '') ? (requested as typeof emphases[number]) : 'primary';
24
+ const target = readOptionalString(element, 'targetScreen');
25
+
26
+ return (
27
+ <span className='foundation foundation-action' data-scene-id={element.id}>
28
+ <ActionButton rule={readPermissionRule(element)} emphasis={emphasis} disabledReason={readOptionalString(element, 'disabledReason') ?? strings.disabledNoPermission}
29
+ onClick={event => {
30
+ if (interactions?.onClick) interactions.onClick(event);
31
+ else if (target && typeof window !== 'undefined') window.location.hash = `#/${target}`;
32
+ }}>
33
+ {readOptionalString(element, 'label') ?? ''}
34
+ </ActionButton>
35
+ </span>
36
+ );
37
+ }
@@ -0,0 +1,138 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { useCallback, useContext, useEffect, useId, useRef } from 'react';
5
+ import { RegisteredComponentProps, SceneNavigationContextInternal } from '@cratis/scene.react';
6
+ import { readOptionalString, readStrings } from '../shell/elementProperties';
7
+ import { FoundationIcon } from './FoundationIcon';
8
+ import { formatText, readFoundationStrings } from './foundationStrings';
9
+ import { isRightToLeft, readLocale } from './formatting';
10
+ import { useCompactWidth } from './useCompactWidth';
11
+
12
+ /** Where a dialog is drawn: in the middle of the page, as a drawer from the inline end, or filling the page. */
13
+ export enum DialogPlacement {
14
+ Center = 'center',
15
+ Side = 'side',
16
+ Full = 'full',
17
+ }
18
+
19
+ /** What a dialog is about: an ordinary one, or one that cannot be undone and is drawn in the danger colors. */
20
+ export enum DialogTone {
21
+ Default = 'default',
22
+ Danger = 'danger',
23
+ }
24
+
25
+ const focusable = 'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])';
26
+
27
+ /** Reads the placement property, falling back to the center for anything that is not one of the three. */
28
+ export function readDialogPlacement(element: RegisteredComponentProps['element']): DialogPlacement {
29
+ const value = readOptionalString(element, 'placement');
30
+ return value === DialogPlacement.Side || value === DialogPlacement.Full ? value : DialogPlacement.Center;
31
+ }
32
+
33
+ /** The placement a dialog is drawn with in a window of the given width class: a phone gets every dialog at full height and width (spec Part B 2.10, 1.5). */
34
+ export const placementFor = (requested: DialogPlacement, compact: boolean): DialogPlacement => (compact ? DialogPlacement.Full : requested);
35
+
36
+ /** The elements a dialog's focus can rest on, in document order, that are not hidden. */
37
+ const focusableIn = (host: HTMLElement): HTMLElement[] => Array.from(host.querySelectorAll<HTMLElement>(focusable)).filter(candidate => !candidate.hidden && candidate.closest('[hidden]') === null);
38
+
39
+ /**
40
+ * The `dialogFrame` component: the head of a dialog template and everything that makes what the host draws around it behave as a dialog.
41
+ *
42
+ * It draws the heading (`title`, `subtitle`), an optional `message` and `consequences` list - what a confirmation says will happen - and a close button.
43
+ * The host draws the box a dialog template opens in (a `role="dialog" aria-modal="true"` element); this component takes that element on and gives it the
44
+ * parts of a dialog that must not differ from one template to the next:
45
+ *
46
+ * - it is named by the heading, takes focus when it opens (the first field, else the primary action, else the close button), keeps Tab inside it, and
47
+ * gives focus back to whatever opened it when it goes;
48
+ * - Escape and the close button run the behaviors attached to this element (a `CloseDialog` action), and close the dialog themselves when none is attached;
49
+ * - `placement` is `center` (default), `side` (a drawer from the inline end, which a right-to-left locale mirrors) or `full`; on a phone every dialog fills the page;
50
+ * - `tone` `danger` draws a dialog that cannot be undone in the danger colors.
51
+ *
52
+ * Every text it says comes from the strings table and the locale of the element, so an application localizes it like any foundation component.
53
+ */
54
+ export function DialogFrame({ element, interactions }: RegisteredComponentProps) {
55
+ const frame = useRef<HTMLElement>(null);
56
+ const heading = useId();
57
+ const navigation = useContext(SceneNavigationContextInternal);
58
+ const compact = useCompactWidth();
59
+ const strings = readFoundationStrings(element);
60
+ const locale = readLocale(element);
61
+ const title = readOptionalString(element, 'title') ?? '';
62
+ const subtitle = readOptionalString(element, 'subtitle');
63
+ const message = readOptionalString(element, 'message');
64
+ const consequences = readStrings(element, 'consequences');
65
+ const placement = placementFor(readDialogPlacement(element), compact);
66
+ const tone = element.properties.tone === DialogTone.Danger ? DialogTone.Danger : DialogTone.Default;
67
+
68
+ const onClick = interactions?.onClick;
69
+ const close = useCallback(() => {
70
+ if (onClick) onClick({ stopPropagation: () => undefined });
71
+ else navigation?.closeDialog();
72
+ }, [onClick, navigation]);
73
+ const closeRef = useRef(close);
74
+ closeRef.current = close;
75
+
76
+ useEffect(() => {
77
+ const host = frame.current?.closest<HTMLElement>('[role="dialog"]');
78
+ if (!host) return;
79
+
80
+ const opener = document.activeElement instanceof HTMLElement ? document.activeElement : undefined;
81
+ const before = { className: host.className, labelledBy: host.getAttribute('aria-labelledby'), placement: host.getAttribute('data-placement'), tone: host.getAttribute('data-tone') };
82
+ host.classList.add('foundation', 'foundation-dialog-host');
83
+ host.setAttribute('aria-labelledby', heading);
84
+ host.setAttribute('data-placement', placement);
85
+ host.setAttribute('data-tone', tone);
86
+ host.dir = isRightToLeft(locale) ? 'rtl' : host.dir;
87
+
88
+ const closeButton = frame.current?.querySelector<HTMLElement>('.foundation-dialog-close');
89
+ const targets = focusableIn(host);
90
+ (targets.find(target => /^(INPUT|SELECT|TEXTAREA)$/.test(target.tagName)) ?? targets.find(target => target !== closeButton) ?? closeButton)?.focus();
91
+
92
+ const onKeyDown = (event: KeyboardEvent) => {
93
+ if (event.key === 'Escape') {
94
+ event.preventDefault();
95
+ closeRef.current();
96
+ } else if (event.key === 'Tab') {
97
+ const inside = focusableIn(host);
98
+ if (inside.length === 0) { event.preventDefault(); return; }
99
+ const first = inside[0];
100
+ const last = inside[inside.length - 1];
101
+ if (event.shiftKey && (document.activeElement === first || !host.contains(document.activeElement))) { event.preventDefault(); last.focus(); }
102
+ else if (!event.shiftKey && (document.activeElement === last || !host.contains(document.activeElement))) { event.preventDefault(); first.focus(); }
103
+ }
104
+ };
105
+
106
+ host.addEventListener('keydown', onKeyDown);
107
+ return () => {
108
+ host.removeEventListener('keydown', onKeyDown);
109
+ host.className = before.className;
110
+ for (const [name, value] of [['aria-labelledby', before.labelledBy], ['data-placement', before.placement], ['data-tone', before.tone]] as const) {
111
+ if (value === null) host.removeAttribute(name);
112
+ else host.setAttribute(name, value);
113
+ }
114
+
115
+ if (opener?.isConnected) opener.focus();
116
+ };
117
+ }, [heading, placement, tone, locale]);
118
+
119
+ return (
120
+ <header ref={frame} className='foundation foundation-dialog-frame' data-scene-id={element.id} data-placement={placement} data-tone={tone} lang={locale} dir={isRightToLeft(locale) ? 'rtl' : undefined}>
121
+ <div className='foundation-dialog-heading'>
122
+ <div className='foundation-dialog-titles'>
123
+ <h2 id={heading} className='foundation-dialog-title'>{title}</h2>
124
+ {subtitle && <p className='foundation-dialog-subtitle'>{subtitle}</p>}
125
+ </div>
126
+ <button type='button' className='foundation-dialog-close' aria-label={formatText(strings.dialogClose, { title })} onClick={close}>
127
+ <FoundationIcon name='close' />
128
+ </button>
129
+ </div>
130
+ {message && <p className='foundation-dialog-message'>{message}</p>}
131
+ {consequences.length > 0 && (
132
+ <ul className='foundation-dialog-consequences' aria-label={strings.dialogConsequences}>
133
+ {consequences.map(consequence => <li key={consequence}>{consequence}</li>)}
134
+ </ul>
135
+ )}
136
+ </header>
137
+ );
138
+ }
@@ -73,6 +73,9 @@ function FieldInput({ field, value, error, readOnly, locale, required, choose, s
73
73
  case FormFieldType.Currency:
74
74
  input = <input {...common} type='number' inputMode='decimal' value={text} placeholder={field.placeholder} onChange={event => set(event.target.value === '' ? '' : Number(event.target.value))} />;
75
75
  break;
76
+ case FormFieldType.Password:
77
+ input = <input {...common} type='password' autoComplete='current-password' value={text} placeholder={field.placeholder} onChange={event => set(event.target.value)} />;
78
+ break;
76
79
  case FormFieldType.Date:
77
80
  input = <input {...common} type='date' lang={locale} value={text} onChange={event => set(event.target.value)} />;
78
81
  break;
@@ -97,9 +100,9 @@ function FieldInput({ field, value, error, readOnly, locale, required, choose, s
97
100
  * unsaved-changes guard.
98
101
  *
99
102
  * `sections` (`title`, `columns`, `fields`) and `values` come from the screen; a field is `text`, `number`, `currency`, `date`, `select`, `multiSelect`, `lookup`,
100
- * `toggle`, `file`, `richText` or `address`. Validation is inline on blur and a summary on submit (an alert listing each field as a link back to it); a server's
103
+ * `toggle`, `file`, `richText`, `address` or `password`. Validation is inline on blur and a summary on submit (an alert listing each field as a link back to it); a server's
101
104
  * `errors` map onto the fields, and one for a field the form lacks is shown as a general error so it is never lost. Required fields are marked with an asterisk
102
- * *and* the word. `mode` is `create`, `edit`, `readOnly` or `settings` - settings has one Save for each section. The edited values are the `value` output; Save
105
+ * *and* the word. `saveAndNew` false leaves out "Save and new" (a dialog that creates one record). `mode` is `create`, `edit`, `readOnly` or `settings` - settings has one Save for each section. The edited values are the `value` output; Save
103
106
  * runs the behaviors attached to the element. While values differ from the saved ones the form says so, warns the browser before the page is left, and offers
104
107
  * "Discard changes". The action bar sticks to the bottom as the form scrolls.
105
108
  */
@@ -185,7 +188,7 @@ export function FormScreenBody({ element, interactions, bindingOutputs }: Regist
185
188
  <span className='foundation-wizard-spacer' />
186
189
  {dirty && <ActionButton emphasis='quiet' disabledReason={strings.disabledNoPermission} onClick={() => { setValues(saved); setTouched(new Set()); setSubmitted(false); }}>{strings.formDiscard}</ActionButton>}
187
190
  <ActionButton emphasis='secondary' disabledReason={strings.disabledNoPermission} onClick={() => interactions?.onClick?.({ stopPropagation: () => undefined })}>{strings.formCancel}</ActionButton>
188
- {mode === FormMode.Create && <ActionButton rule={canSave} emphasis='secondary' disabledReason={strings.disabledNoPermission} onClick={() => { submit(); }}>{strings.formSaveAndNew}</ActionButton>}
191
+ {mode === FormMode.Create && element.properties.saveAndNew !== false && <ActionButton rule={canSave} emphasis='secondary' disabledReason={strings.disabledNoPermission} onClick={() => { submit(); }}>{strings.formSaveAndNew}</ActionButton>}
189
192
  <ActionButton rule={canSave} disabledReason={strings.disabledNoPermission} onClick={() => submit()}>{strings.formSave}</ActionButton>
190
193
  </div>
191
194
  )}
@@ -0,0 +1,33 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { RegisteredComponentProps } from '@cratis/scene.react';
5
+ import { readOptionalString } from '../shell/elementProperties';
6
+ import { FoundationIcon } from './FoundationIcon';
7
+ import { readLocale } from './formatting';
8
+ import { StatusTone, readStatusTone, statusToneIcons } from './StatusChip';
9
+
10
+ /**
11
+ * The `notice` component: one message about the region it sits in - a sign-in error, a warning above a form, a note under a field.
12
+ *
13
+ * It is a live region: a danger or warning notice is an `alert` and any other a `status`, and the region is on the page even while empty, so the
14
+ * message is announced when it arrives rather than only when the page loads. With no `text` nothing is drawn but the empty region, which is what a
15
+ * sign-in screen's error area is until a sign-in fails. `tone` picks the icon and the status colors; the message is always words, never color alone.
16
+ */
17
+ export function Notice({ element }: RegisteredComponentProps) {
18
+ const tone = readStatusTone(element.properties.tone ?? StatusTone.Danger);
19
+ const text = readOptionalString(element, 'text') ?? '';
20
+ const title = readOptionalString(element, 'title');
21
+ const urgent = tone === StatusTone.Danger || tone === StatusTone.Warning;
22
+
23
+ return (
24
+ <div className='foundation foundation-notice' data-scene-id={element.id} data-tone={tone} data-empty={text === '' || undefined} role={urgent ? 'alert' : 'status'} lang={readLocale(element)}>
25
+ {text !== '' && (
26
+ <>
27
+ <FoundationIcon name={statusToneIcons[tone]} />
28
+ <p>{title && <strong>{title} </strong>}{text}</p>
29
+ </>
30
+ )}
31
+ </div>
32
+ );
33
+ }
@@ -14,6 +14,7 @@ export enum FormFieldType {
14
14
  File = 'file',
15
15
  RichText = 'richText',
16
16
  Address = 'address',
17
+ Password = 'password',
17
18
  }
18
19
 
19
20
  /** One field of a form section. */
@@ -28,6 +28,9 @@ export const foundationComponentMetadata: Record<string, ComponentMetadata> = {
28
28
  [ComponentName.DataList]: m('Data list', ComponentCategory.DataDisplay, 'table', ['table', 'grid', 'records', 'cards', 'collection', 'list']),
29
29
  [ComponentName.TabSet]: m('Tab set', ComponentCategory.Navigation, 'panel-top', ['tabs', 'sections', 'panels']),
30
30
  [ComponentName.SubNavigation]: m('Sub-navigation', ComponentCategory.Navigation, 'panel-left', ['module', 'sections', 'rail', 'tabs']),
31
+ [ComponentName.DialogFrame]: m('Dialog frame', ComponentCategory.Feedback, 'panel-top', ['dialog', 'modal', 'sheet', 'drawer', 'confirm', 'header']),
32
+ [ComponentName.ActionButton]: m('Action button', ComponentCategory.Actions, 'mouse-pointer-click', ['button', 'action', 'submit', 'cancel', 'confirm']),
33
+ [ComponentName.Notice]: m('Notice', ComponentCategory.Feedback, 'circle-alert', ['message', 'error', 'alert', 'banner', 'warning']),
31
34
  [ComponentName.BulkBar]: m('Bulk bar', ComponentCategory.Actions, 'check-check', ['selection', 'bulk', 'batch', 'actions']),
32
35
  [ComponentName.GlobalSearch]: m('Search', ComponentCategory.Input, 'search', ['search', 'find', 'query']),
33
36
  [ComponentName.NotificationsButton]: m('Notifications button', ComponentCategory.Actions, 'bell', ['bell', 'alerts', 'unread']),
@@ -434,6 +434,46 @@ export const foundationDescriptors: ComponentDescriptor[] = [
434
434
  ...standardStateProperties.filter(property => property.path !== 'label'),
435
435
  ],
436
436
  },
437
+ {
438
+ component: key(ComponentName.DialogFrame),
439
+ displayName: 'Dialog frame',
440
+ description: 'The head of a dialog: its title, a message and the consequences it states, and a close button. It makes the dialog it opens in keep focus inside, restore it, close on Escape and be drawn centered, as a side sheet or full height.',
441
+ properties: [
442
+ text('title', 'Title', 'Content', undefined, { constraints: { required: true } }),
443
+ text('subtitle', 'Subtitle', 'Content'),
444
+ text('message', 'Message', 'Content', 'What the dialog asks or says.'),
445
+ { path: 'consequences', label: 'Consequences', group: 'Content', valueType: PropertyValueType.Json, editorKind: PropertyEditorKind.StringList, default: [], description: 'What will happen if the person goes ahead, one line each.' },
446
+ choice('placement', 'Placement', 'Appearance', { center: 'Centered', side: 'Side sheet', full: 'Full height' }, 'center', 'Where the dialog is drawn; on a phone every dialog fills the page.'),
447
+ choice('tone', 'Tone', 'Appearance', { default: 'Default', danger: 'Destructive' }, 'default', 'A destructive dialog is drawn in the danger colors.'),
448
+ text('locale', 'Locale', 'Content'),
449
+ { path: 'strings', label: 'Text overrides', group: 'Content', valueType: PropertyValueType.Object, editorKind: PropertyEditorKind.StringMap },
450
+ ],
451
+ },
452
+ {
453
+ component: key(ComponentName.ActionButton),
454
+ displayName: 'Action button',
455
+ description: 'One action, drawn as every action of the foundation is, that is enabled, disabled with its reason, or hidden by the permission of the user.',
456
+ properties: [
457
+ text('label', 'Label', 'Content', 'What it does - a verb, never "OK".', { constraints: { required: true } }),
458
+ choice('emphasis', 'Emphasis', 'Appearance', { primary: 'Primary', secondary: 'Secondary', quiet: 'Quiet', danger: 'Destructive' }, 'primary'),
459
+ rule('permission', 'Permission', 'Whether the action works, refuses with a reason or is not shown.'),
460
+ text('disabledReason', 'Reason when disabled', 'Permissions'),
461
+ text('targetScreen', 'Go to screen', 'Behavior', 'The screen it goes to when no behavior is attached.'),
462
+ text('locale', 'Locale', 'Content'),
463
+ { path: 'strings', label: 'Text overrides', group: 'Content', valueType: PropertyValueType.Object, editorKind: PropertyEditorKind.StringMap },
464
+ ],
465
+ },
466
+ {
467
+ component: key(ComponentName.Notice),
468
+ displayName: 'Notice',
469
+ description: 'One message about the region it sits in, announced when it arrives. Empty until it has something to say.',
470
+ properties: [
471
+ text('text', 'Message', 'Data', 'Bind to the error or note to show; empty draws nothing.'),
472
+ text('title', 'Title', 'Content'),
473
+ choice('tone', 'Tone', 'Appearance', { danger: 'Error', warning: 'Warning', info: 'Information', success: 'Success', neutral: 'Neutral' }, 'danger'),
474
+ text('locale', 'Locale', 'Content'),
475
+ ],
476
+ },
437
477
  {
438
478
  component: key(ComponentName.BulkBar),
439
479
  displayName: 'Bulk bar',
@@ -521,6 +561,7 @@ export const foundationDescriptors: ComponentDescriptor[] = [
521
561
  displayName: 'Form',
522
562
  description: 'Titled sections of fields in a one-to-three column grid, with inline validation, a save bar, the unsaved-changes guard and four modes.',
523
563
  properties: [
564
+ flag('saveAndNew', 'Offer Save and new', 'Behavior', 'Whether a create form offers Save and new next to Save; a dialog that creates one record leaves it out.', { default: true }),
524
565
  {
525
566
  ...list('sections', 'Sections', 'Data', 'Section', [
526
567
  text('id', 'Id', 'Data'),
@@ -131,6 +131,9 @@ export const foundationSamples: Record<string, FoundationSample> = {
131
131
  properties: { items: [{ id: 'accounts', label: 'Accounts' }, { id: 'contacts', label: 'Contacts', badge: '12' }, { id: 'deals', label: 'Deals' }] },
132
132
  data: ['Accounts', 'Contacts'], stateful: true,
133
133
  },
134
+ [ComponentName.DialogFrame]: { properties: { title: 'Archive product', message: 'Bamboo Watch will stop appearing in the catalog.', consequences: ['Existing orders keep it', 'It can be restored from the archive'], tone: 'danger' }, data: [], stateful: false },
135
+ [ComponentName.ActionButton]: { properties: { label: 'Archive product', emphasis: 'danger' }, data: [], stateful: false },
136
+ [ComponentName.Notice]: { properties: { text: 'The email or password is not right.', tone: 'danger' }, data: [], stateful: false },
134
137
  [ComponentName.BulkBar]: { properties: { selection: ['1', '2'], actions: ['Assign', 'Archive'] }, data: [], stateful: false },
135
138
  [ComponentName.GlobalSearch]: { properties: { placeholder: 'Search accounts', label: 'Search accounts' }, data: [], stateful: false },
136
139
  [ComponentName.NotificationsButton]: { properties: { count: 3 }, data: [], stateful: false },
@@ -203,6 +203,8 @@ export const foundationStrings = {
203
203
  matrixPermission: 'Permission',
204
204
  matrixScroll: 'Permission matrix, scrolls sideways',
205
205
  matrixGroupAll: 'Other',
206
+ dialogClose: 'Close {title}',
207
+ dialogConsequences: 'What will happen',
206
208
  } as const;
207
209
 
208
210
  /** The name of one of the foundation strings. */
@@ -43,6 +43,9 @@ export * from './DataList';
43
43
  export * from './tabModel';
44
44
  export * from './TabSet';
45
45
  export * from './SubNavigation';
46
+ export * from './DialogFrame';
47
+ export * from './Notice';
48
+ export * from './ActionButtonControl';
46
49
  export * from './BulkBar';
47
50
  export * from './GlobalSearch';
48
51
  export * from './NotificationsButton';
@@ -56,3 +59,4 @@ export * from './foundationDescriptors';
56
59
  export * from './foundationComponentMetadata';
57
60
  export * from './foundationSamples';
58
61
  export * from './FoundationStates';
62
+ export * from './templates';
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ScreenTemplate, WidthSizeClass } from '@cratis/scene.model';
5
+ import { column, grid, row, slotLeaf } from '../../layouts/flowBuilders';
6
+ import { component, foundationTemplateProvenance, fromQuery, thumbnailOf } from './templateParts';
7
+
8
+ /**
9
+ * The board: columns mapped to a status or stage, cards that move between them - by dragging or by each card's own Move-to menu, so the board is as
10
+ * usable from a keyboard and a screen reader as with a pointer.
11
+ */
12
+ export const boardScreenTemplate: ScreenTemplate = {
13
+ name: 'BoardScreen',
14
+ fitsSlot: 'content',
15
+ slots: [{ name: 'header' }, { name: 'toolbar' }, { name: 'board' }],
16
+ arrangement: {
17
+ root: column([slotLeaf('header'), row([slotLeaf('toolbar')], 8), slotLeaf('board')], 12),
18
+ overrides: [{ width: WidthSizeClass.Compact, root: column([slotLeaf('header'), slotLeaf('toolbar'), slotLeaf('board')], 16) }],
19
+ },
20
+ content: {
21
+ header: [component('board-header', 'pageHeader', { title: 'Pipeline', subtitle: 'Drag a deal to a new stage' })],
22
+ toolbar: [component('board-search', 'globalSearch', { placeholder: 'Search the pipeline', label: 'Search the pipeline' })],
23
+ board: [component('board-columns', 'kanbanBoard', { columns: fromQuery('PipelineColumns'), cards: fromQuery('PipelineCards') })],
24
+ },
25
+ displayName: 'Board',
26
+ description: 'Columns mapped to a stage with cards that move by dragging or by menu.',
27
+ metadata: { ...foundationTemplateProvenance, type: 'Board', category: 'Foundation / Board', thumbnail: thumbnailOf('BoardScreen') },
28
+ };
@@ -0,0 +1,37 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ScreenTemplate, WidthSizeClass } from '@cratis/scene.model';
5
+ import { column, grid, row, slotLeaf } from '../../layouts/flowBuilders';
6
+ import { component, foundationTemplateProvenance, fromQuery, thumbnailOf } from './templateParts';
7
+
8
+ /**
9
+ * The dashboard: key figures across the top, charts and tables below, an aside for what needs attention. Every region is its own component with its
10
+ * own five states, so one failed query shows the other regions - the page never fails as a whole.
11
+ */
12
+ export const dashboardScreenTemplate: ScreenTemplate = {
13
+ name: 'DashboardScreen',
14
+ fitsSlot: 'content',
15
+ slots: [{ name: 'header' }, { name: 'kpis' }, { name: 'charts' }, { name: 'tables' }, { name: 'aside' }],
16
+ arrangement: {
17
+ root: column([slotLeaf('header'), slotLeaf('kpis'), grid([slotLeaf('charts', { span: 2 }), slotLeaf('aside')], 3, 16), slotLeaf('tables')], 16),
18
+ overrides: [{ width: WidthSizeClass.Compact, root: column([slotLeaf('header'), slotLeaf('kpis'), slotLeaf('charts'), slotLeaf('tables'), slotLeaf('aside')], 16) }],
19
+ },
20
+ content: {
21
+ header: [component('dashboard-header', 'pageHeader', { title: 'Sales overview', subtitle: 'This quarter compared to last' })],
22
+ kpis: [
23
+ component('kpi-pipeline', 'statCard', { label: 'Open pipeline', value: fromQuery('Kpis', '0.value'), change: fromQuery('Kpis', '0.change'), format: 'currency', currency: 'USD', comparison: 'last quarter' }),
24
+ component('kpi-won', 'statCard', { label: 'Deals won', value: fromQuery('Kpis', '1.value'), change: fromQuery('Kpis', '1.change'), comparison: 'last quarter' }),
25
+ component('kpi-response', 'statCard', { label: 'Response time (hours)', value: fromQuery('Kpis', '2.value'), change: fromQuery('Kpis', '2.change'), goodDirection: 'down', comparison: 'last quarter' }),
26
+ ],
27
+ charts: [
28
+ component('chart-revenue', 'chartWrapper', { title: 'Revenue by quarter', kind: 'line', categories: fromQuery('RevenueChart', '0.categories'), series: fromQuery('RevenueChart', '0.series') }),
29
+ component('chart-stages', 'chartWrapper', { title: 'Deals by stage', kind: 'funnel', categories: fromQuery('StageChart', '0.categories'), series: fromQuery('StageChart', '0.series') }),
30
+ ],
31
+ tables: [component('dashboard-deals', 'dataList', { label: 'Closing soon', rows: fromQuery('Deals'), columns: fromQuery('DealColumns'), titleField: 'name', pageSize: 5 })],
32
+ aside: [component('dashboard-activity', 'activityFeed', { label: 'Recent activity', entries: fromQuery('Activity') })],
33
+ },
34
+ displayName: 'Dashboard',
35
+ description: 'Key figures, charts and tables, each region with its own loading, empty and error states.',
36
+ metadata: { ...foundationTemplateProvenance, type: 'Dashboard', category: 'Foundation / Dashboard', thumbnail: thumbnailOf('DashboardScreen') },
37
+ };
@@ -0,0 +1,114 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Behavior, DialogTemplate, ExternalComponent, InteractionActionKind, InteractionTriggerKind } from '@cratis/scene.model';
5
+ import { panel } from '../../gallery/elements';
6
+ import { defaultBlueprintTemplateProvenance } from '../../templateProvenance';
7
+ import { component, foundationTemplateProvenance, fromQuery, thumbnailOf } from './templateParts';
8
+
9
+ /** What a Cancel or Close control does: close the dialog the screen opened it from, in Scene, in Stage and in the preview alike. */
10
+ export const closeDialogBehaviors: Behavior[] = [{ bindings: [{ trigger: { kind: InteractionTriggerKind.Click }, actions: [{ kind: InteractionActionKind.CloseDialog }] }] }];
11
+
12
+ /** An element that closes the dialog when it is clicked. */
13
+ export const closingDialog = <T extends ExternalComponent>(element: T): T => ({ ...element, behaviors: closeDialogBehaviors });
14
+
15
+ /**
16
+ * The provenance, type and category every foundation dialog carries. A dialog built from the foundation and core components needs no component library; one that
17
+ * uses PrimeReact widgets (`needsPrimeReact`) declares the packages it requires.
18
+ */
19
+ export const dialogMetadata = (name: string, needsPrimeReact = false) =>
20
+ ({ ...(needsPrimeReact ? defaultBlueprintTemplateProvenance : foundationTemplateProvenance), type: 'Dialog', category: 'Foundation / Dialog', thumbnail: thumbnailOf(name) });
21
+
22
+ /** One action of the foundation, by the id it is known by. */
23
+ export const actionButton = (id: string, properties: Record<string, unknown>): ExternalComponent => component(id, 'actionButton', properties);
24
+
25
+ /** The footer of a dialog: its buttons, the one that confirms last, so it is the one the eye ends on. */
26
+ export const dialogActions = (id: string, buttons: ExternalComponent[]) => panel(`${id}-actions`, buttons);
27
+
28
+ /**
29
+ * The quick-create dialog: a short form - six fields at most - opened from a list's header or from a lookup, to make one record without leaving the page.
30
+ * Cancel closes it, Create validates and saves. `FormDialog` is the general form dialog this one is the focused form of: the same shape with the heading,
31
+ * focus handling and the form of the foundation, which is why it is offered as the alias a quick-create button chooses.
32
+ */
33
+ export const quickCreateDialogTemplate: DialogTemplate = {
34
+ name: 'QuickCreateDialog',
35
+ slots: [{ name: 'header' }, { name: 'body' }, { name: 'actions' }],
36
+ content: {
37
+ header: [component('quick-create-frame', 'dialogFrame', { title: 'New contact', subtitle: 'Only what is needed to get started' })],
38
+ body: [closingDialog(component('quick-create-form', 'formScreenBody', {
39
+ mode: 'create', saveAndNew: false, strings: { formSave: 'Create' },
40
+ sections: fromQuery('QuickCreateSections'), values: fromQuery('QuickCreateValues', '0'), errors: fromQuery('FormErrors', '0'),
41
+ }))],
42
+ actions: [],
43
+ },
44
+ displayName: 'Quick create',
45
+ description: 'A short form of six fields at most with Cancel and Create, opened without leaving the page.',
46
+ metadata: dialogMetadata('QuickCreateDialog'),
47
+ };
48
+
49
+ /**
50
+ * The lookup dialog: search, a table or list, and single or multiple selection, with Cancel and Select. Any lookup field opens it, so one dialog serves them all;
51
+ * `selectionMode` on the list is `single` (the default here) or `multiple`.
52
+ */
53
+ export const lookupDialogTemplate: DialogTemplate = {
54
+ name: 'LookupDialog',
55
+ slots: [{ name: 'header' }, { name: 'body' }, { name: 'actions' }],
56
+ content: {
57
+ header: [component('lookup-frame', 'dialogFrame', { title: 'Select an account', subtitle: 'Search, then choose one' })],
58
+ body: [
59
+ component('lookup-search', 'globalSearch', { placeholder: 'Search accounts', label: 'Search accounts' }),
60
+ component('lookup-list', 'dataList', { label: 'Accounts', rows: fromQuery('LookupRows'), columns: fromQuery('LookupColumns'), titleField: 'name', selectionMode: 'single', pageSize: 6 }),
61
+ ],
62
+ actions: [dialogActions('lookup', [
63
+ closingDialog(actionButton('lookup-cancel', { label: 'Cancel', emphasis: 'secondary' })),
64
+ closingDialog(actionButton('lookup-select', { label: 'Select', emphasis: 'primary' })),
65
+ ])],
66
+ },
67
+ displayName: 'Lookup',
68
+ description: 'A search and a list to choose one or several records from, with Cancel and Select.',
69
+ metadata: dialogMetadata('LookupDialog'),
70
+ };
71
+
72
+ /**
73
+ * The side sheet: a drawer from the inline end with a header, a scrollable body and footer actions. It hosts what a page hosts - a form, or a record's
74
+ * tabs and related records - so a record can be edited or read from beside the list it came from.
75
+ */
76
+ export const sideSheetTemplate: DialogTemplate = {
77
+ name: 'SideSheet',
78
+ slots: [{ name: 'header' }, { name: 'body' }, { name: 'actions' }],
79
+ content: {
80
+ header: [component('sheet-frame', 'dialogFrame', { title: 'Account details', subtitle: 'Edit without leaving the list', placement: 'side' })],
81
+ body: [closingDialog(component('sheet-form', 'formScreenBody', { mode: 'edit', sections: fromQuery('RecordSections'), values: fromQuery('RecordValues', '0'), errors: fromQuery('FormErrors', '0') }))],
82
+ actions: [dialogActions('sheet', [
83
+ closingDialog(actionButton('sheet-close', { label: 'Close', emphasis: 'secondary' })),
84
+ actionButton('sheet-open', { label: 'Open full page', emphasis: 'primary', targetScreen: 'RecordPage' }),
85
+ ])],
86
+ },
87
+ displayName: 'Side sheet',
88
+ description: 'A drawer from the side hosting a record or a form, with footer actions.',
89
+ metadata: dialogMetadata('SideSheet'),
90
+ };
91
+
92
+ /**
93
+ * The filter dialog: the advanced filter builder in a dialog. On a phone it fills the page, which makes it the phone's alternative to the filter panel
94
+ * a list shows beside its table.
95
+ */
96
+ export const filterDialogTemplate: DialogTemplate = {
97
+ name: 'FilterDialog',
98
+ slots: [{ name: 'header' }, { name: 'body' }, { name: 'actions' }],
99
+ content: {
100
+ header: [component('filter-frame', 'dialogFrame', { title: 'Filters', subtitle: 'Show only the deals that match' })],
101
+ body: [component('filter-builder', 'filterBuilder', { fields: fromQuery('DealFilterFields'), filter: fromQuery('DealFilter', '0') })],
102
+ actions: [dialogActions('filter', [
103
+ actionButton('filter-reset', { label: 'Reset', emphasis: 'secondary' }),
104
+ closingDialog(actionButton('filter-cancel', { label: 'Cancel', emphasis: 'secondary' })),
105
+ closingDialog(actionButton('filter-apply', { label: 'Apply filters', emphasis: 'primary' })),
106
+ ])],
107
+ },
108
+ displayName: 'Filters',
109
+ description: 'The advanced filter builder in a dialog, the phone alternative to the filter panel.',
110
+ metadata: dialogMetadata('FilterDialog'),
111
+ };
112
+
113
+ /** The dialogs the foundation adds to the three the gallery already had. */
114
+ export const foundationDialogTemplates: DialogTemplate[] = [quickCreateDialogTemplate, lookupDialogTemplate, sideSheetTemplate, filterDialogTemplate];
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { ScreenTemplate, WidthSizeClass } from '@cratis/scene.model';
5
+ import { column, grid, row, slotLeaf } from '../../layouts/flowBuilders';
6
+ import { component, foundationTemplateProvenance, fromQuery, thumbnailOf } from './templateParts';
7
+
8
+ /**
9
+ * The form: titled sections of fields in a one-to-three column grid, validated, with Save, Save and new and Cancel. One template serves create, edit, read-only
10
+ * and settings (a save for each section) by its mode, and warns before unsaved changes are lost.
11
+ */
12
+ export const formScreenTemplate: ScreenTemplate = {
13
+ name: 'FormScreen',
14
+ fitsSlot: 'content',
15
+ slots: [{ name: 'header' }, { name: 'body' }, { name: 'actions' }],
16
+ arrangement: {
17
+ root: column([slotLeaf('header'), slotLeaf('body'), slotLeaf('actions')], 16),
18
+ overrides: [{ width: WidthSizeClass.Compact, root: column([slotLeaf('header'), slotLeaf('body'), slotLeaf('actions')], 16) }],
19
+ },
20
+ content: {
21
+ header: [component('form-header', 'pageHeader', { title: 'New account', subtitle: 'Fields marked * are required' })],
22
+ body: [component('form-body', 'formScreenBody', { mode: 'create', sections: fromQuery('RecordSections'), values: fromQuery('RecordValues', '0'), errors: fromQuery('FormErrors', '0') })],
23
+ actions: [],
24
+ },
25
+ displayName: 'Form',
26
+ description: 'Sections of validated fields with Save, Save and new, Cancel and an unsaved-changes guard.',
27
+ metadata: { ...foundationTemplateProvenance, type: 'Form', category: 'Foundation / Form', thumbnail: thumbnailOf('FormScreen') },
28
+ };
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { boardScreenTemplate } from './boardScreenTemplate';
5
+ import { dashboardScreenTemplate } from './dashboardScreenTemplate';
6
+ import { formScreenTemplate } from './formScreenTemplate';
7
+ import { listScreenTemplate } from './listScreenTemplate';
8
+ import { masterDetailScreenTemplate } from './masterDetailScreenTemplate';
9
+ import { recordPageTemplate } from './recordPageTemplate';
10
+ import { wizardScreenTemplate } from './wizardScreenTemplate';
11
+ import { workspaceScreenTemplate } from './workspaceScreenTemplate';
12
+
13
+ export * from './templateParts';
14
+ export * from './dialogTemplates';
15
+ export * from './stateScreenTemplates';
16
+ export * from './boardScreenTemplate';
17
+ export * from './dashboardScreenTemplate';
18
+ export * from './formScreenTemplate';
19
+ export * from './listScreenTemplate';
20
+ export * from './masterDetailScreenTemplate';
21
+ export * from './recordPageTemplate';
22
+ export * from './wizardScreenTemplate';
23
+ export * from './workspaceScreenTemplate';
24
+
25
+ /** The foundation screen templates, in the order the manifest lists them. The workspace comes first: it is the container the master/detail nests in. */
26
+ export const foundationScreenTemplates = [
27
+ workspaceScreenTemplate, dashboardScreenTemplate, listScreenTemplate, boardScreenTemplate, masterDetailScreenTemplate, recordPageTemplate, formScreenTemplate, wizardScreenTemplate,
28
+ ];