@cratis/scene.primereact 4.5.0 → 4.6.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 (156) hide show
  1. package/README.md +25 -3
  2. package/chart/ChartGallery.stories.tsx +63 -0
  3. package/chart/ChartType.ts +14 -0
  4. package/chart/ChartTypeResolution.ts +14 -0
  5. package/chart/PrimeChart.tsx +62 -0
  6. package/chart/chartHasData.ts +22 -0
  7. package/chart/chartTypeChoices.ts +32 -0
  8. package/chart/index.ts +7 -0
  9. package/chart/resolveChartType.ts +61 -0
  10. package/chart/useChart.ts +61 -0
  11. package/dist/cjs/chart/ChartType.js +18 -0
  12. package/dist/cjs/chart/ChartType.js.map +1 -0
  13. package/dist/cjs/chart/PrimeChart.js +92 -0
  14. package/dist/cjs/chart/PrimeChart.js.map +1 -0
  15. package/dist/cjs/chart/chartHasData.js +24 -0
  16. package/dist/cjs/chart/chartHasData.js.map +1 -0
  17. package/dist/cjs/chart/chartTypeChoices.js +43 -0
  18. package/dist/cjs/chart/chartTypeChoices.js.map +1 -0
  19. package/dist/cjs/chart/resolveChartType.js +69 -0
  20. package/dist/cjs/chart/resolveChartType.js.map +1 -0
  21. package/dist/cjs/chart/useChart.js +62 -0
  22. package/dist/cjs/chart/useChart.js.map +1 -0
  23. package/dist/cjs/form/MultiStateGlyph.js +59 -0
  24. package/dist/cjs/form/MultiStateGlyph.js.map +1 -0
  25. package/dist/cjs/form/PrimeMultiStateCheckbox.js +123 -0
  26. package/dist/cjs/form/PrimeMultiStateCheckbox.js.map +1 -0
  27. package/dist/cjs/form/multiStateOptions.js +77 -0
  28. package/dist/cjs/form/multiStateOptions.js.map +1 -0
  29. package/dist/cjs/form/multiStateStates.js +32 -0
  30. package/dist/cjs/form/multiStateStates.js.map +1 -0
  31. package/dist/cjs/form/useMultiStateSelection.js +51 -0
  32. package/dist/cjs/form/useMultiStateSelection.js.map +1 -0
  33. package/dist/cjs/index.js +12 -0
  34. package/dist/cjs/index.js.map +1 -1
  35. package/dist/cjs/primeReactComponents.js +5 -0
  36. package/dist/cjs/primeReactComponents.js.map +1 -1
  37. package/dist/cjs/primeReactDescriptors.js +157 -0
  38. package/dist/cjs/primeReactDescriptors.js.map +1 -0
  39. package/dist/cjs/primeReactPackage.js +4 -0
  40. package/dist/cjs/primeReactPackage.js.map +1 -1
  41. package/dist/cjs/properties.js +11 -0
  42. package/dist/cjs/properties.js.map +1 -1
  43. package/dist/cjs/structurallyEqual.js +27 -0
  44. package/dist/cjs/structurallyEqual.js.map +1 -0
  45. package/dist/cjs/useStructuralValue.js +20 -0
  46. package/dist/cjs/useStructuralValue.js.map +1 -0
  47. package/dist/esm/chart/ChartGallery.stories.d.ts +14 -0
  48. package/dist/esm/chart/ChartGallery.stories.d.ts.map +1 -0
  49. package/dist/esm/chart/ChartGallery.stories.js +47 -0
  50. package/dist/esm/chart/ChartGallery.stories.js.map +1 -0
  51. package/dist/esm/chart/ChartType.d.ts +11 -0
  52. package/dist/esm/chart/ChartType.d.ts.map +1 -0
  53. package/dist/esm/chart/ChartType.js +16 -0
  54. package/dist/esm/chart/ChartType.js.map +1 -0
  55. package/dist/esm/chart/ChartTypeResolution.d.ts +10 -0
  56. package/dist/esm/chart/ChartTypeResolution.d.ts.map +1 -0
  57. package/dist/esm/chart/ChartTypeResolution.js +2 -0
  58. package/dist/esm/chart/ChartTypeResolution.js.map +1 -0
  59. package/dist/esm/chart/PrimeChart.d.ts +3 -0
  60. package/dist/esm/chart/PrimeChart.d.ts.map +1 -0
  61. package/dist/esm/chart/PrimeChart.js +90 -0
  62. package/dist/esm/chart/PrimeChart.js.map +1 -0
  63. package/dist/esm/chart/chartHasData.d.ts +2 -0
  64. package/dist/esm/chart/chartHasData.d.ts.map +1 -0
  65. package/dist/esm/chart/chartHasData.js +22 -0
  66. package/dist/esm/chart/chartHasData.js.map +1 -0
  67. package/dist/esm/chart/chartTypeChoices.d.ts +3 -0
  68. package/dist/esm/chart/chartTypeChoices.d.ts.map +1 -0
  69. package/dist/esm/chart/chartTypeChoices.js +41 -0
  70. package/dist/esm/chart/chartTypeChoices.js.map +1 -0
  71. package/dist/esm/chart/index.d.ts +5 -0
  72. package/dist/esm/chart/index.d.ts.map +1 -0
  73. package/dist/esm/chart/index.js +5 -0
  74. package/dist/esm/chart/index.js.map +1 -0
  75. package/dist/esm/chart/resolveChartType.d.ts +6 -0
  76. package/dist/esm/chart/resolveChartType.d.ts.map +1 -0
  77. package/dist/esm/chart/resolveChartType.js +66 -0
  78. package/dist/esm/chart/resolveChartType.js.map +1 -0
  79. package/dist/esm/chart/useChart.d.ts +4 -0
  80. package/dist/esm/chart/useChart.d.ts.map +1 -0
  81. package/dist/esm/chart/useChart.js +60 -0
  82. package/dist/esm/chart/useChart.js.map +1 -0
  83. package/dist/esm/form/FormGallery.stories.d.ts.map +1 -1
  84. package/dist/esm/form/FormGallery.stories.js +8 -0
  85. package/dist/esm/form/FormGallery.stories.js.map +1 -1
  86. package/dist/esm/form/MultiStateGlyph.d.ts +8 -0
  87. package/dist/esm/form/MultiStateGlyph.d.ts.map +1 -0
  88. package/dist/esm/form/MultiStateGlyph.js +57 -0
  89. package/dist/esm/form/MultiStateGlyph.js.map +1 -0
  90. package/dist/esm/form/MultiStateOption.d.ts +7 -0
  91. package/dist/esm/form/MultiStateOption.d.ts.map +1 -0
  92. package/dist/esm/form/MultiStateOption.js +2 -0
  93. package/dist/esm/form/MultiStateOption.js.map +1 -0
  94. package/dist/esm/form/PrimeMultiStateCheckbox.d.ts +3 -0
  95. package/dist/esm/form/PrimeMultiStateCheckbox.d.ts.map +1 -0
  96. package/dist/esm/form/PrimeMultiStateCheckbox.js +121 -0
  97. package/dist/esm/form/PrimeMultiStateCheckbox.js.map +1 -0
  98. package/dist/esm/form/index.d.ts +1 -0
  99. package/dist/esm/form/index.d.ts.map +1 -1
  100. package/dist/esm/form/index.js +1 -0
  101. package/dist/esm/form/index.js.map +1 -1
  102. package/dist/esm/form/multiStateOptions.d.ts +4 -0
  103. package/dist/esm/form/multiStateOptions.d.ts.map +1 -0
  104. package/dist/esm/form/multiStateOptions.js +75 -0
  105. package/dist/esm/form/multiStateOptions.js.map +1 -0
  106. package/dist/esm/form/multiStateStates.d.ts +4 -0
  107. package/dist/esm/form/multiStateStates.d.ts.map +1 -0
  108. package/dist/esm/form/multiStateStates.js +30 -0
  109. package/dist/esm/form/multiStateStates.js.map +1 -0
  110. package/dist/esm/form/useMultiStateSelection.d.ts +6 -0
  111. package/dist/esm/form/useMultiStateSelection.d.ts.map +1 -0
  112. package/dist/esm/form/useMultiStateSelection.js +49 -0
  113. package/dist/esm/form/useMultiStateSelection.js.map +1 -0
  114. package/dist/esm/index.d.ts +2 -0
  115. package/dist/esm/index.d.ts.map +1 -1
  116. package/dist/esm/index.js +6 -1
  117. package/dist/esm/index.js.map +1 -1
  118. package/dist/esm/primeReactComponents.d.ts.map +1 -1
  119. package/dist/esm/primeReactComponents.js +5 -0
  120. package/dist/esm/primeReactComponents.js.map +1 -1
  121. package/dist/esm/primeReactDescriptors.d.ts +3 -0
  122. package/dist/esm/primeReactDescriptors.d.ts.map +1 -0
  123. package/dist/esm/primeReactDescriptors.js +155 -0
  124. package/dist/esm/primeReactDescriptors.js.map +1 -0
  125. package/dist/esm/primeReactPackage.d.ts.map +1 -1
  126. package/dist/esm/primeReactPackage.js +4 -0
  127. package/dist/esm/primeReactPackage.js.map +1 -1
  128. package/dist/esm/properties.d.ts +1 -0
  129. package/dist/esm/properties.d.ts.map +1 -1
  130. package/dist/esm/properties.js +11 -1
  131. package/dist/esm/properties.js.map +1 -1
  132. package/dist/esm/structurallyEqual.d.ts +2 -0
  133. package/dist/esm/structurallyEqual.d.ts.map +1 -0
  134. package/dist/esm/structurallyEqual.js +25 -0
  135. package/dist/esm/structurallyEqual.js.map +1 -0
  136. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  137. package/dist/esm/useStructuralValue.d.ts +2 -0
  138. package/dist/esm/useStructuralValue.d.ts.map +1 -0
  139. package/dist/esm/useStructuralValue.js +18 -0
  140. package/dist/esm/useStructuralValue.js.map +1 -0
  141. package/form/FormGallery.stories.tsx +8 -0
  142. package/form/MultiStateGlyph.tsx +55 -0
  143. package/form/MultiStateOption.ts +17 -0
  144. package/form/PrimeMultiStateCheckbox.tsx +101 -0
  145. package/form/index.ts +1 -0
  146. package/form/multiStateOptions.ts +78 -0
  147. package/form/multiStateStates.ts +23 -0
  148. package/form/useMultiStateSelection.ts +48 -0
  149. package/index.ts +2 -0
  150. package/package.json +17 -7
  151. package/primeReactComponents.ts +4 -0
  152. package/primeReactDescriptors.ts +48 -0
  153. package/primeReactPackage.ts +4 -2
  154. package/properties.ts +14 -0
  155. package/structurallyEqual.ts +29 -0
  156. package/useStructuralValue.ts +17 -0
@@ -0,0 +1,2 @@
1
+ export declare function useStructuralValue<TValue>(value: TValue): TValue;
2
+ //# sourceMappingURL=useStructuralValue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStructuralValue.d.ts","sourceRoot":"","sources":["../../useStructuralValue.ts"],"names":[],"mappings":"AAYA,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhE"}
@@ -0,0 +1,18 @@
1
+ import { useRef } from 'react';
2
+ import { structurallyEqual } from './structurallyEqual.js';
3
+
4
+ // Copyright (c) Cratis. All rights reserved.
5
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
6
+ /**
7
+ * Returns the same reference for as long as the content of `value` is unchanged.
8
+ *
9
+ * Use it for authored data that feeds an effect dependency list: a cloned document gives that data a new
10
+ * identity on every unrelated edit, which would otherwise rerun the effect and rebuild whatever it owns.
11
+ */ function useStructuralValue(value) {
12
+ const stable = useRef(value);
13
+ if (!structurallyEqual(stable.current, value)) stable.current = value;
14
+ return stable.current;
15
+ }
16
+
17
+ export { useStructuralValue };
18
+ //# sourceMappingURL=useStructuralValue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStructuralValue.js","sources":["../../useStructuralValue.ts"],"sourcesContent":["// Copyright (c) Cratis. All rights reserved.\n// Licensed under the MIT license. See LICENSE file in the project root for full license information.\n\nimport { useRef } from 'react';\nimport { structurallyEqual } from './structurallyEqual';\n\n/**\n * Returns the same reference for as long as the content of `value` is unchanged.\n *\n * Use it for authored data that feeds an effect dependency list: a cloned document gives that data a new\n * identity on every unrelated edit, which would otherwise rerun the effect and rebuild whatever it owns.\n */\nexport function useStructuralValue<TValue>(value: TValue): TValue {\n const stable = useRef(value);\n if (!structurallyEqual(stable.current, value)) stable.current = value;\n return stable.current;\n}\n"],"names":["useStructuralValue","value","stable","useRef","structurallyEqual","current"],"mappings":";;;AAAA;AACA;AAKA;;;;;IAMO,SAASA,kBAAAA,CAA2BC,KAAa,EAAA;AACpD,IAAA,MAAMC,SAASC,MAAAA,CAAOF,KAAAA,CAAAA;IACtB,IAAI,CAACG,kBAAkBF,MAAAA,CAAOG,OAAO,EAAEJ,KAAAA,CAAAA,EAAQC,MAAAA,CAAOG,OAAO,GAAGJ,KAAAA;AAChE,IAAA,OAAOC,OAAOG,OAAO;AACzB;;"}
@@ -35,6 +35,14 @@ export const Gallery: Story = {
35
35
  sceneComponent('listBox', 'listBox', { options: ['Daily', 'Weekly', 'Monthly'] }),
36
36
  sceneComponent('selectButton', 'selectButton', { options: ['Day', 'Week', 'Month'], value: 'Week' }),
37
37
  sceneComponent('checkbox', 'checkbox', { label: 'Notify me', checked: true }),
38
+ sceneComponent('multiStateCheckbox', 'multiStateCheckbox', {
39
+ value: null,
40
+ options: [
41
+ { label: 'Approved', value: 'approved', icon: 'pi pi-check' },
42
+ { label: 'Rejected', value: 'rejected', icon: 'pi pi-times' },
43
+ ],
44
+ ariaLabel: 'Review state',
45
+ }),
38
46
  sceneComponent('radioButton', 'radioButton', { options: ['Email', 'SMS'], value: 'Email' }),
39
47
  sceneComponent('toggleSwitch', 'toggleSwitch', { label: 'Enabled', checked: true }),
40
48
  sceneComponent('slider', 'slider', { value: 35 }),
@@ -0,0 +1,55 @@
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 { SceneIcon } from '@cratis/scene.react';
5
+ import { MultiStateOption } from './MultiStateOption';
6
+
7
+ export interface MultiStateGlyphProps {
8
+ /** The state being shown. */
9
+ state: MultiStateOption | undefined;
10
+
11
+ /** Whether the state is the empty (`null`) state, drawn as an empty box. */
12
+ isEmpty: boolean;
13
+
14
+ /** Whether the control is disabled or read only. */
15
+ isInactive: boolean;
16
+ }
17
+
18
+ /**
19
+ * The box a multi-state checkbox draws, themed with the PrimeReact checkbox tokens (`--p-checkbox-*`), so
20
+ * it follows the active Scene theme and looks like its sibling `checkbox`. It holds the state's icon - a
21
+ * PrimeIcons class or a qualified icon reference - or a check mark when the state has no icon of its own.
22
+ * An empty state draws only its own icon, if it has one. It is decorative: the state is announced through the control's text, not through this glyph.
23
+ */
24
+ export function MultiStateGlyph({ state, isEmpty, isInactive }: MultiStateGlyphProps) {
25
+ const icon = state?.icon;
26
+ return (
27
+ <span
28
+ aria-hidden='true'
29
+ data-scene-part='glyph'
30
+ style={{
31
+ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, boxSizing: 'border-box',
32
+ width: 'var(--p-checkbox-width, 1.125rem)', height: 'var(--p-checkbox-height, 1.125rem)',
33
+ borderRadius: 'var(--p-checkbox-border-radius, 4px)',
34
+ border: `1px solid ${isEmpty ? 'var(--p-checkbox-border-color, currentColor)' : 'var(--p-checkbox-checked-border-color, currentColor)'}`,
35
+ backgroundColor: isEmpty ? 'var(--p-checkbox-background, transparent)' : 'var(--p-checkbox-checked-background, currentColor)',
36
+ color: glyphColor(isEmpty, isInactive),
37
+ fontSize: 'var(--p-checkbox-icon-size, 0.75rem)',
38
+ }}>
39
+ {icon === undefined ? !isEmpty && <CheckMark /> : typeof icon === 'string' ? <i className={icon} /> : <SceneIcon reference={icon} size='var(--p-checkbox-icon-size, 0.75rem)' />}
40
+ </span>
41
+ );
42
+ }
43
+
44
+ function glyphColor(isEmpty: boolean, isInactive: boolean): string {
45
+ if (isInactive) return 'var(--p-checkbox-icon-disabled-color, currentColor)';
46
+ return isEmpty ? 'var(--p-checkbox-icon-color, currentColor)' : 'var(--p-checkbox-icon-checked-color, Canvas)';
47
+ }
48
+
49
+ function CheckMark() {
50
+ return (
51
+ <svg viewBox='0 0 12 12' width='1em' height='1em' fill='none' stroke='currentColor' strokeWidth='2' strokeLinecap='round' strokeLinejoin='round'>
52
+ <path d='M2 6.5 5 9.5 10 3' />
53
+ </svg>
54
+ );
55
+ }
@@ -0,0 +1,17 @@
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 { IconReference } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * One state a {@link PrimeMultiStateCheckbox} can cycle through.
8
+ *
9
+ * The `value` is exactly what the document authored - any JSON value, including `null`, numbers, booleans
10
+ * and objects. The `icon` is either a legacy PrimeIcons class name (`'pi pi-check'`) or a qualified
11
+ * {@link IconReference}.
12
+ */
13
+ export interface MultiStateOption {
14
+ label: string;
15
+ value: unknown;
16
+ icon?: string | IconReference;
17
+ }
@@ -0,0 +1,101 @@
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 { CSSProperties, useId, useState } from 'react';
5
+ import { isIconReference } from '@cratis/scene.model';
6
+ import { RegisteredComponentProps } from '@cratis/scene.react';
7
+ import { booleanProperty, stringProperty } from '../properties';
8
+ import { MultiStateGlyph } from './MultiStateGlyph';
9
+ import { multiStateOptions } from './multiStateOptions';
10
+ import { multiStateStates } from './multiStateStates';
11
+ import { useMultiStateSelection } from './useMultiStateSelection';
12
+
13
+ const visuallyHidden: CSSProperties = {
14
+ position: 'absolute', width: 1, height: 1, margin: -1, padding: 0, overflow: 'hidden', clip: 'rect(0 0 0 0)', whiteSpace: 'nowrap', border: 0,
15
+ };
16
+
17
+ /**
18
+ * The `PrimeReact:multiStateCheckbox` component.
19
+ *
20
+ * PrimeReact 11 removed MultiStateCheckbox, so Scene renders its original behavior with a native button.
21
+ * It cycles through the authored states in order, including the optional empty (`null`) state, and keeps
22
+ * option values and icons exactly as the element carries them. Each click moves one step; the position is
23
+ * an index, so duplicated or `null` option values do not make later states unreachable.
24
+ *
25
+ * Accessibility: a state cycle is not a true/false choice, so the control does not claim `checkbox`
26
+ * semantics (every non-empty state would announce as "checked"). It is a button whose name is the authored
27
+ * `ariaLabel`, whose description is the current state, and whose state changes are announced through a
28
+ * polite live region. `readOnly` keeps the control focusable and unchanged; `disabled` removes it from the
29
+ * tab order.
30
+ *
31
+ * The control follows the document: a changed `value` or `options` restarts the cycle from the new value,
32
+ * so an inspector edit is reflected immediately. It does not report its own selection to behaviors -
33
+ * Scene's interaction handlers carry no value.
34
+ */
35
+ export function PrimeMultiStateCheckbox({ element, interactions }: RegisteredComponentProps) {
36
+ const stateId = useId();
37
+ const allowEmpty = booleanProperty(element, 'empty', true);
38
+ const emptyLabel = stringProperty(element, 'emptyLabel', 'No selection');
39
+ const emptyIcon = element.properties.emptyIcon;
40
+ const states = multiStateStates(
41
+ multiStateOptions(element, emptyLabel),
42
+ allowEmpty,
43
+ emptyLabel,
44
+ typeof emptyIcon === 'string' || isIconReference(emptyIcon) ? emptyIcon : undefined);
45
+ const authored = element.properties.value;
46
+ const { index, advance } = useMultiStateSelection(states, authored === undefined && allowEmpty ? null : authored);
47
+ const [focused, setFocused] = useState(false);
48
+
49
+ const selected = states[index];
50
+ const readOnly = booleanProperty(element, 'readOnly', false);
51
+ const disabled = !element.isEnabled || booleanProperty(element, 'disabled', false) || states.length === 0;
52
+ const ariaLabel = stringProperty(element, 'ariaLabel', element.name || 'Multi-state checkbox');
53
+ const stateText = selected?.label ?? stringProperty(element, 'placeholder', unmatchedText(states.length, authored, emptyLabel));
54
+
55
+ return (
56
+ <>
57
+ <button
58
+ type='button'
59
+ data-scene-id={element.id}
60
+ data-scene-component='multiStateCheckbox'
61
+ data-scene-state-index={index}
62
+ aria-label={ariaLabel}
63
+ aria-describedby={stateId}
64
+ aria-disabled={readOnly ? true : undefined}
65
+ disabled={disabled}
66
+ style={{
67
+ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', padding: 0, border: 0, background: 'none', font: 'inherit',
68
+ color: 'inherit', cursor: disabled ? 'not-allowed' : readOnly ? 'default' : 'pointer', opacity: disabled ? 0.6 : 1,
69
+ outline: focused ? 'var(--p-focus-ring-width, 2px) var(--p-focus-ring-style, solid) var(--p-focus-ring-color, currentColor)' : 'none',
70
+ outlineOffset: 'var(--p-focus-ring-offset, 2px)',
71
+ }}
72
+ onFocus={(event) => setFocused(isFocusVisible(event.currentTarget))}
73
+ onBlur={() => setFocused(false)}
74
+ onClick={(event) => {
75
+ if (!readOnly) advance();
76
+ interactions?.onClick?.(event);
77
+ if (readOnly) return;
78
+ interactions?.onChange?.();
79
+ interactions?.onSelect?.();
80
+ }}
81
+ onDoubleClick={interactions?.onDoubleClick}>
82
+ <MultiStateGlyph state={selected} isEmpty={selected === undefined || selected.value === null} isInactive={disabled || readOnly} />
83
+ <span aria-hidden='true'>{stateText}</span>
84
+ </button>
85
+ <span id={stateId} role='status' style={visuallyHidden}>{stateText}</span>
86
+ </>
87
+ );
88
+ }
89
+
90
+ function unmatchedText(stateCount: number, authored: unknown, emptyLabel: string): string {
91
+ if (stateCount === 0) return 'No states';
92
+ return authored === undefined ? emptyLabel : 'No matching state';
93
+ }
94
+
95
+ function isFocusVisible(target: HTMLElement): boolean {
96
+ try {
97
+ return target.matches(':focus-visible');
98
+ } catch {
99
+ return true;
100
+ }
101
+ }
package/form/index.ts CHANGED
@@ -12,6 +12,7 @@ export * from './PrimeMultiSelect';
12
12
  export * from './PrimeListBox';
13
13
  export * from './PrimeSelectButton';
14
14
  export * from './PrimeCheckbox';
15
+ export * from './PrimeMultiStateCheckbox';
15
16
  export * from './PrimeRadioButton';
16
17
  export * from './PrimeToggleSwitch';
17
18
  export * from './PrimeSlider';
@@ -0,0 +1,78 @@
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 { SceneElement, isIconReference } from '@cratis/scene.model';
5
+ import { arrayProperty, stringProperty } from '../properties';
6
+ import { MultiStateOption } from './MultiStateOption';
7
+
8
+ /**
9
+ * Reads multi-state options without reshaping their values, dropping any of them, or changing their order.
10
+ *
11
+ * This follows the original PrimeReact control. An option that is a record gives its value through
12
+ * `optionValue` (default, and when that field is absent: its `value` field, and the whole option when it has none) and its label through
13
+ * `optionLabel` (default: its `label` field). Any other option - a string, number, boolean, `null` or an
14
+ * array - is its own value. A label that is not text is shown as its JSON, so a numeric or boolean label,
15
+ * or an option with no label at all, still produces a state the user can reach. A `null` value with no
16
+ * label of its own is labeled with `emptyLabel`.
17
+ *
18
+ * A state's icon is, in order: the option's own `icon`, then the entry of the element's `icons` that is
19
+ * aligned with the option (an array) or keyed by its value (an object). Either may be a PrimeIcons class
20
+ * name, kept for legacy documents, or a qualified {@link IconReference}.
21
+ */
22
+ export function multiStateOptions(element: SceneElement, emptyLabel: string): MultiStateOption[] {
23
+ const labelPath = stringProperty(element, 'optionLabel');
24
+ const valuePath = stringProperty(element, 'optionValue');
25
+ const icons = element.properties.icons;
26
+
27
+ return arrayProperty(element, 'options').map((option, index) => {
28
+ const record = isRecord(option) ? option : undefined;
29
+ const value = record === undefined ? option : valueOf(record, valuePath);
30
+ const label = record === undefined ? undefined : resolvePath(record, labelPath ?? 'label');
31
+ const icon = toIcon(record?.icon) ?? iconAt(icons, value, index);
32
+ return { label: labelText(label, value, emptyLabel), value, ...(icon === undefined ? {} : { icon }) };
33
+ });
34
+ }
35
+
36
+ function valueOf(record: Record<string, unknown>, valuePath: string | undefined): unknown {
37
+ const authored = valuePath === undefined ? undefined : resolvePath(record, valuePath);
38
+ if (authored !== undefined) return authored;
39
+ return record.value !== undefined ? record.value : record;
40
+ }
41
+
42
+ function resolvePath(record: Record<string, unknown>, path: string): unknown {
43
+ if (Object.hasOwn(record, path)) return record[path];
44
+ let current: unknown = record;
45
+ for (const segment of path.split('.')) {
46
+ if (!isRecord(current) || !Object.hasOwn(current, segment)) return undefined;
47
+ current = current[segment];
48
+ }
49
+ return current;
50
+ }
51
+
52
+ function labelText(label: unknown, value: unknown, emptyLabel: string): string {
53
+ if (typeof label === 'string' && label.length > 0) return label;
54
+ if (label !== undefined && label !== null && label !== '') return display(label);
55
+ return value === null ? emptyLabel : display(value);
56
+ }
57
+
58
+ function display(value: unknown): string {
59
+ const text = typeof value === 'string' ? value : JSON.stringify(value) ?? String(value);
60
+ return text === '' ? '""' : text;
61
+ }
62
+
63
+ function toIcon(candidate: unknown): MultiStateOption['icon'] {
64
+ if (typeof candidate === 'string' && candidate.length > 0) return candidate;
65
+ return isIconReference(candidate) ? candidate : undefined;
66
+ }
67
+
68
+ function iconAt(icons: unknown, value: unknown, index: number): MultiStateOption['icon'] {
69
+ if (Array.isArray(icons)) return toIcon(icons[index]);
70
+ if (!isRecord(icons)) return undefined;
71
+
72
+ const isKey = value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
73
+ return isKey ? toIcon(icons[String(value)]) : undefined;
74
+ }
75
+
76
+ function isRecord(value: unknown): value is Record<string, unknown> {
77
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
78
+ }
@@ -0,0 +1,23 @@
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 { IconReference } from '@cratis/scene.model';
5
+ import { MultiStateOption } from './MultiStateOption';
6
+
7
+ /**
8
+ * Builds the ordered states a multi-state checkbox cycles through.
9
+ *
10
+ * With the empty state allowed, a state whose value is `null` comes first - unless an authored option
11
+ * already has the value `null`, in which case that option *is* the empty state and keeps its authored
12
+ * position, label and icon. Adding a second `null` state would leave two states with one value, and a
13
+ * control that finds its place by value could then never move past the first of them.
14
+ *
15
+ * @param options The authored options, in order.
16
+ * @param allowEmpty Whether a `null` state is available.
17
+ * @param emptyLabel The label of the synthesized empty state.
18
+ * @param emptyIcon The icon of the synthesized empty state.
19
+ */
20
+ export function multiStateStates(options: MultiStateOption[], allowEmpty: boolean, emptyLabel: string, emptyIcon: string | IconReference | undefined): MultiStateOption[] {
21
+ if (!allowEmpty || options.some(option => option.value === null)) return options;
22
+ return [{ label: emptyLabel, value: null, ...(emptyIcon === undefined ? {} : { icon: emptyIcon }) }, ...options];
23
+ }
@@ -0,0 +1,48 @@
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 { useState } from 'react';
5
+ import { structurallyEqual } from '../structurallyEqual';
6
+ import { MultiStateOption } from './MultiStateOption';
7
+
8
+ interface Selection {
9
+ source: unknown;
10
+ values: unknown[];
11
+ index: number;
12
+ }
13
+
14
+ /**
15
+ * The state a multi-state checkbox is in, as an index into its states.
16
+ *
17
+ * The position is tracked by index, not recovered by looking the current value up: several states can
18
+ * share a value (a duplicated option), and a lookup by value always finds the first of them, which would
19
+ * make every later state unreachable. The index follows the cycle; the value is only used to find the
20
+ * starting position.
21
+ *
22
+ * The control is driven by the document as well as by the user. When the authored `value` or the states
23
+ * change in content - an edit in the inspector, a hydrated document - the selection starts again from the
24
+ * new value. A document clone with equal content changes nothing, so the user's position survives it.
25
+ *
26
+ * @param states The states, in cycle order.
27
+ * @param value The authored current value.
28
+ * @returns The selected index (`-1` when the value matches no state) and a function that moves to the next.
29
+ */
30
+ export function useMultiStateSelection(states: MultiStateOption[], value: unknown): { index: number; advance(): number } {
31
+ const values = states.map(state => state.value);
32
+ const start = (): Selection => ({ source: value, values, index: values.findIndex(candidate => structurallyEqual(candidate, value)) });
33
+ const [selection, setSelection] = useState<Selection>(start);
34
+
35
+ let current = selection;
36
+ if (!structurallyEqual(selection.source, value) || !structurallyEqual(selection.values, values)) {
37
+ current = start();
38
+ setSelection(current);
39
+ }
40
+
41
+ const advance = () => {
42
+ const index = states.length === 0 ? -1 : (current.index + 1) % states.length;
43
+ setSelection({ ...current, index });
44
+ return index;
45
+ };
46
+
47
+ return { index: current.index, advance };
48
+ }
package/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './menuItems';
10
10
  export * from './treeNodes';
11
11
  export * from './button';
12
12
  export * from './form';
13
+ export * from './chart';
13
14
  export * from './data';
14
15
  export * from './panel';
15
16
  export * from './overlay';
@@ -20,4 +21,5 @@ export * from './misc';
20
21
  export * from './screen';
21
22
  export * from './theme';
22
23
  export * from './primeReactComponents';
24
+ export * from './primeReactDescriptors';
23
25
  export * from './primeReactPackage';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.primereact",
3
- "version": "4.5.0",
3
+ "version": "4.6.0",
4
4
  "description": "The PrimeReact component package: maps Scene's abstract component names onto real PrimeReact components, and PrimeReact's free themes onto Scene themes.",
5
5
  "keywords": [
6
6
  "cratis",
@@ -47,27 +47,31 @@
47
47
  "lint": "yarn g:lint",
48
48
  "lint:ci": "yarn g:lint:ci",
49
49
  "test": "yarn g:test",
50
+ "test:browser": "yarn g:test --config ./vitest.browser.config.mts",
50
51
  "ci": "yarn g:ci",
51
52
  "up": "yarn g:up",
52
53
  "dev": "storybook dev -p 6006",
53
54
  "build-storybook": "storybook build"
54
55
  },
55
56
  "dependencies": {
56
- "@cratis/scene.engine": "4.5.0",
57
- "@cratis/scene.model": "4.5.0",
58
- "@cratis/scene.react": "4.5.0",
57
+ "@cratis/scene.engine": "4.6.0",
58
+ "@cratis/scene.model": "4.6.0",
59
+ "@cratis/scene.react": "4.6.0",
59
60
  "@primeuix/themes": "^3.0.1"
60
61
  },
61
62
  "devDependencies": {
62
- "@cratis/scene.engine": "4.5.0",
63
- "@cratis/scene.model": "4.5.0",
64
- "@cratis/scene.react": "4.5.0",
63
+ "@cratis/scene.engine": "4.6.0",
64
+ "@cratis/scene.model": "4.6.0",
65
+ "@cratis/scene.react": "4.6.0",
65
66
  "@primereact/core": "11.2.0",
66
67
  "@primereact/headless": "11.2.0",
67
68
  "@primereact/types": "11.2.0",
68
69
  "@storybook/addon-links": "^10.6.1",
69
70
  "@storybook/react": "^10.6.1",
70
71
  "@storybook/react-vite": "^10.6.1",
72
+ "chart.js": "4.5.1",
73
+ "esbuild": "0.28.2",
74
+ "playwright-core": "1.63.0",
71
75
  "primeicons": "8.0.2",
72
76
  "primereact": "11.2.0",
73
77
  "react": "^19.3.0",
@@ -77,9 +81,15 @@
77
81
  "peerDependencies": {
78
82
  "@primereact/core": "^11.0.0",
79
83
  "@primereact/headless": "^11.0.0",
84
+ "chart.js": "^4.5.1",
80
85
  "primeicons": "^8.0.0",
81
86
  "primereact": "^11.0.0",
82
87
  "react": "^19.0.0",
83
88
  "react-dom": "^19.0.0"
89
+ },
90
+ "peerDependenciesMeta": {
91
+ "chart.js": {
92
+ "optional": true
93
+ }
84
94
  }
85
95
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { ComponentRegistry, componentRegistryKey } from '@cratis/scene.react';
5
5
  import { PrimeButton, PrimeButtonGroup, PrimeSpeedDial, PrimeSplitButton } from './button';
6
+ import { PrimeChart } from './chart';
6
7
  import {
7
8
  PrimeColumn,
8
9
  PrimeDataTable,
@@ -33,6 +34,7 @@ import {
33
34
  PrimeRadioButton,
34
35
  PrimeRating,
35
36
  PrimeSelectButton,
37
+ PrimeMultiStateCheckbox,
36
38
  PrimeSlider,
37
39
  PrimeToggleSwitch,
38
40
  PrimeTreeSelect,
@@ -108,6 +110,7 @@ export const primeReactComponents: ComponentRegistry = {
108
110
  [componentRegistryKey('PrimeReact', 'listBox')]: PrimeListBox,
109
111
  [componentRegistryKey('PrimeReact', 'selectButton')]: PrimeSelectButton,
110
112
  [componentRegistryKey('PrimeReact', 'checkbox')]: PrimeCheckbox,
113
+ [componentRegistryKey('PrimeReact', 'multiStateCheckbox')]: PrimeMultiStateCheckbox,
111
114
  [componentRegistryKey('PrimeReact', 'radioButton')]: PrimeRadioButton,
112
115
  [componentRegistryKey('PrimeReact', 'toggleSwitch')]: PrimeToggleSwitch,
113
116
  [componentRegistryKey('PrimeReact', 'slider')]: PrimeSlider,
@@ -134,6 +137,7 @@ export const primeReactComponents: ComponentRegistry = {
134
137
  [componentRegistryKey('PrimeReact', 'orderList')]: PrimeOrderList,
135
138
  [componentRegistryKey('PrimeReact', 'pickList')]: PrimePickList,
136
139
  [componentRegistryKey('PrimeReact', 'organizationChart')]: PrimeOrganizationChart,
140
+ [componentRegistryKey('PrimeReact', 'chart')]: PrimeChart,
137
141
 
138
142
  [componentRegistryKey('PrimeReact', 'card')]: PrimeCard,
139
143
  [componentRegistryKey('PrimeReact', 'panel')]: PrimePanel,
@@ -0,0 +1,48 @@
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 { ComponentDescriptor, PropertyValueType } from '@cratis/scene.model';
5
+ import { componentRegistryKey } from '@cratis/scene.react';
6
+ import { chartTypeChoices } from './chart/chartTypeChoices';
7
+ import { ChartType } from './chart';
8
+
9
+ const key = (name: string) => componentRegistryKey('PrimeReact', name);
10
+
11
+ /** Design-time descriptors for PrimeReact controls whose authored data has structured configuration. */
12
+ export const primeReactDescriptors: ComponentDescriptor[] = [
13
+ {
14
+ component: key('chart'),
15
+ displayName: 'Chart',
16
+ description: 'A Chart.js chart. Legacy numeric types are rendered without changing stored data; new documents use stable type names. The host application must install chart.js.',
17
+ properties: [
18
+ {
19
+ path: 'type', label: 'Type', group: 'Chart', valueType: PropertyValueType.Enum, default: ChartType.Bar,
20
+ choices: chartTypeChoices,
21
+ },
22
+ { path: 'data', label: 'Data', group: 'Data', valueType: PropertyValueType.Json, description: 'The Chart.js data object, including labels and datasets in their authored order.' },
23
+ { path: 'options', label: 'Options', group: 'Chart', valueType: PropertyValueType.Json, description: 'The Chart.js options object.' },
24
+ { path: 'emptyLabel', label: 'No data label', group: 'Accessibility', valueType: PropertyValueType.String, default: 'No chart data', description: 'Shown instead of the chart while it has no data points. Data is never invented.' },
25
+ { path: 'responsive', label: 'Responsive', group: 'Layout', valueType: PropertyValueType.Boolean, default: true },
26
+ { path: 'style', label: 'Style', group: 'Layout', valueType: PropertyValueType.Json, description: 'The CSS style object applied to the chart surface.' },
27
+ { path: 'ariaLabel', label: 'Accessible name', group: 'Accessibility', valueType: PropertyValueType.String },
28
+ ],
29
+ },
30
+ {
31
+ component: key('multiStateCheckbox'),
32
+ displayName: 'Multi-state checkbox',
33
+ description: 'An accessible button that cycles through its authored states in order, showing the current state as a checkbox-style glyph.',
34
+ properties: [
35
+ { path: 'value', label: 'Value', group: 'State', valueType: PropertyValueType.Json, description: 'The selected value. It may be null when the empty state is enabled.' },
36
+ { path: 'options', label: 'Options', group: 'State', valueType: PropertyValueType.Json, description: 'The ordered state options. Each may provide label, value, and icon fields.' },
37
+ { path: 'optionLabel', label: 'Option label field', group: 'State', valueType: PropertyValueType.String, default: 'label' },
38
+ { path: 'optionValue', label: 'Option value field', group: 'State', valueType: PropertyValueType.String, default: 'value' },
39
+ { path: 'empty', label: 'Allow empty state', group: 'State', valueType: PropertyValueType.Boolean, default: true },
40
+ { path: 'emptyLabel', label: 'Empty state label', group: 'Accessibility', valueType: PropertyValueType.String, default: 'No selection' },
41
+ { path: 'icons', label: 'Icons', group: 'Appearance', valueType: PropertyValueType.Json, description: 'Icons aligned with the options, or keyed by their values. Each is a PrimeIcons class name or a qualified icon reference. Icons inside this value are not tracked by the icon library diagnostics; use the empty state icon where a tracked icon is needed.' },
42
+ { path: 'emptyIcon', label: 'Empty state icon', group: 'Appearance', valueType: PropertyValueType.Icon },
43
+ { path: 'readOnly', label: 'Read only', group: 'Behavior', valueType: PropertyValueType.Boolean, default: false },
44
+ { path: 'disabled', label: 'Disabled', group: 'Behavior', valueType: PropertyValueType.Boolean, default: false },
45
+ { path: 'ariaLabel', label: 'Accessible name', group: 'Accessibility', valueType: PropertyValueType.String },
46
+ ],
47
+ },
48
+ ];
@@ -4,6 +4,7 @@
4
4
  import { PackageKind, ScenePackage } from '@cratis/scene.model';
5
5
  import { ScenePackageBundle } from '@cratis/scene.react';
6
6
  import { primeReactComponents } from './primeReactComponents';
7
+ import { primeReactDescriptors } from './primeReactDescriptors';
7
8
  import { primeReactThemeNames, primeReactThemes } from './theme';
8
9
 
9
10
  /**
@@ -34,9 +35,9 @@ export const primeReactPackageManifest: ScenePackage = {
34
35
 
35
36
  components: [
36
37
  'inputText', 'inputTextarea', 'inputNumber', 'password', 'floatLabel', 'iconField', 'dropdown', 'multiSelect',
37
- 'listBox', 'selectButton', 'checkbox', 'radioButton', 'toggleSwitch', 'slider', 'rating', 'knob', 'calendar', 'colorPicker',
38
+ 'listBox', 'selectButton', 'checkbox', 'multiStateCheckbox', 'radioButton', 'toggleSwitch', 'slider', 'rating', 'knob', 'calendar', 'colorPicker',
38
39
  'chips', 'autoComplete', 'treeSelect', 'button', 'splitButton', 'speedDial', 'buttonGroup', 'dataTable', 'table',
39
- 'column', 'dataView', 'tree', 'timeline', 'paginator', 'orderList', 'pickList', 'organizationChart',
40
+ 'column', 'dataView', 'tree', 'timeline', 'paginator', 'orderList', 'pickList', 'organizationChart', 'chart',
40
41
  'card', 'panel', 'accordion', 'fieldset', 'divider', 'splitter', 'scrollPanel', 'tabView', 'toolbar', 'stepper', 'dialog',
41
42
  'confirmDialog', 'overlayPanel', 'sidebar', 'tooltip', 'menu', 'menubar', 'breadcrumb', 'tabMenu', 'steps', 'tieredMenu',
42
43
  'panelMenu', 'contextMenu', 'megaMenu', 'dock', 'message', 'inlineMessage', 'toast', 'image', 'galleria', 'carousel', 'avatar',
@@ -85,5 +86,6 @@ export const primeReactPackageManifest: ScenePackage = {
85
86
  export const primeReactPackage: ScenePackageBundle = {
86
87
  manifest: primeReactPackageManifest,
87
88
  components: primeReactComponents,
89
+ descriptors: primeReactDescriptors,
88
90
  themes: primeReactThemes,
89
91
  };
package/properties.ts CHANGED
@@ -106,6 +106,20 @@ export function recordArrayProperty(element: SceneElement, name: string): Record
106
106
  );
107
107
  }
108
108
 
109
+ /**
110
+ * Reads one structured value off a Scene element.
111
+ *
112
+ * Chart data and options intentionally stay as authored objects: these adapters pass their vocabulary to
113
+ * their underlying library instead of choosing a partial Scene-owned chart schema that would discard
114
+ * future Chart.js fields during a legacy migration.
115
+ */
116
+ export function recordProperty(element: SceneElement, name: string): Record<string, unknown> | undefined {
117
+ const value = element.properties[name];
118
+ return typeof value === 'object' && value !== undefined && value !== null && !Array.isArray(value)
119
+ ? value as Record<string, unknown>
120
+ : undefined;
121
+ }
122
+
109
123
  /**
110
124
  * Reads a selection component's options off a Scene element, normalizing both shapes a screen may use.
111
125
  *
@@ -0,0 +1,29 @@
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
+ /**
5
+ * Compares values that can survive a Scene document serialization round trip.
6
+ *
7
+ * Authored data (chart datasets, option values, current values) arrives as plain JSON. Every edit to a
8
+ * Scene document clones the whole document, so the same data gets a new object identity with no change in
9
+ * content; reference equality would treat each unrelated edit as a change. Primitives compare with
10
+ * `Object.is`, so `0` and `-0`, or `NaN` and `NaN`, are told apart exactly as `Object.is` does.
11
+ */
12
+ export function structurallyEqual(left: unknown, right: unknown): boolean {
13
+ if (Object.is(left, right)) return true;
14
+ if (Array.isArray(left) || Array.isArray(right)) {
15
+ return Array.isArray(left) && Array.isArray(right)
16
+ && left.length === right.length
17
+ && left.every((value, index) => structurallyEqual(value, right[index]));
18
+ }
19
+
20
+ if (!isRecord(left) || !isRecord(right)) return false;
21
+ const leftKeys = Object.keys(left).sort();
22
+ const rightKeys = Object.keys(right).sort();
23
+ return leftKeys.length === rightKeys.length
24
+ && leftKeys.every((key, index) => key === rightKeys[index] && structurallyEqual(left[key], right[key]));
25
+ }
26
+
27
+ function isRecord(value: unknown): value is Record<string, unknown> {
28
+ return value !== null && typeof value === 'object';
29
+ }
@@ -0,0 +1,17 @@
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 { useRef } from 'react';
5
+ import { structurallyEqual } from './structurallyEqual';
6
+
7
+ /**
8
+ * Returns the same reference for as long as the content of `value` is unchanged.
9
+ *
10
+ * Use it for authored data that feeds an effect dependency list: a cloned document gives that data a new
11
+ * identity on every unrelated edit, which would otherwise rerun the effect and rebuild whatever it owns.
12
+ */
13
+ export function useStructuralValue<TValue>(value: TValue): TValue {
14
+ const stable = useRef(value);
15
+ if (!structurallyEqual(stable.current, value)) stable.current = value;
16
+ return stable.current;
17
+ }