sh3-core 0.13.1 → 0.13.2

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 (119) hide show
  1. package/dist/BrandSlot.svelte +62 -13
  2. package/dist/__test__/setup-dom.js +5 -0
  3. package/dist/api.d.ts +3 -0
  4. package/dist/api.js +3 -0
  5. package/dist/apps/lifecycle.js +10 -2
  6. package/dist/apps/types.d.ts +11 -4
  7. package/dist/apps/workspace-rekey.d.ts +1 -0
  8. package/dist/apps/workspace-rekey.js +35 -0
  9. package/dist/apps/workspace-rekey.test.js +23 -0
  10. package/dist/auth/admin-users.svelte.d.ts +9 -0
  11. package/dist/auth/admin-users.svelte.js +42 -0
  12. package/dist/auth/admin-users.test.d.ts +1 -0
  13. package/dist/auth/admin-users.test.js +52 -0
  14. package/dist/createShell.js +5 -5
  15. package/dist/documents/config.d.ts +5 -1
  16. package/dist/documents/config.js +16 -8
  17. package/dist/documents/index.d.ts +1 -1
  18. package/dist/documents/index.js +1 -1
  19. package/dist/host-entry.d.ts +1 -1
  20. package/dist/host-entry.js +1 -1
  21. package/dist/host.d.ts +1 -1
  22. package/dist/host.js +8 -2
  23. package/dist/primitives/Button.svelte +50 -4
  24. package/dist/primitives/Button.svelte.d.ts +3 -1
  25. package/dist/primitives/Collapsible.svelte +110 -0
  26. package/dist/primitives/Collapsible.svelte.d.ts +14 -0
  27. package/dist/primitives/widgets/AppPicker.svelte +41 -0
  28. package/dist/primitives/widgets/AppPicker.svelte.d.ts +9 -0
  29. package/dist/primitives/widgets/AppPicker.svelte.test.d.ts +1 -0
  30. package/dist/primitives/widgets/AppPicker.svelte.test.js +26 -0
  31. package/dist/primitives/widgets/AppPicker.test.d.ts +1 -0
  32. package/dist/primitives/widgets/AppPicker.test.js +74 -0
  33. package/dist/primitives/widgets/ColorSwatch.svelte +7 -2
  34. package/dist/primitives/widgets/ColorSwatch.svelte.d.ts +2 -1
  35. package/dist/primitives/widgets/ColorSwatch.svelte.test.d.ts +1 -0
  36. package/dist/primitives/widgets/ColorSwatch.svelte.test.js +31 -0
  37. package/dist/primitives/widgets/Field.svelte +4 -2
  38. package/dist/primitives/widgets/Field.svelte.d.ts +2 -2
  39. package/dist/primitives/widgets/Field.svelte.test.d.ts +1 -0
  40. package/dist/primitives/widgets/Field.svelte.test.js +33 -0
  41. package/dist/primitives/widgets/FilePicker.svelte +2 -2
  42. package/dist/primitives/widgets/FilePicker.svelte.d.ts +2 -2
  43. package/dist/primitives/widgets/FilePicker.svelte.test.d.ts +1 -0
  44. package/dist/primitives/widgets/FilePicker.svelte.test.js +31 -0
  45. package/dist/primitives/widgets/IconToggleGroup.svelte +4 -4
  46. package/dist/primitives/widgets/IconToggleGroup.svelte.d.ts +3 -3
  47. package/dist/primitives/widgets/IconToggleGroup.svelte.test.d.ts +1 -0
  48. package/dist/primitives/widgets/IconToggleGroup.svelte.test.js +40 -0
  49. package/dist/primitives/widgets/NumberInput.svelte +19 -9
  50. package/dist/primitives/widgets/NumberInput.svelte.d.ts +2 -2
  51. package/dist/primitives/widgets/NumberInput.svelte.test.d.ts +1 -0
  52. package/dist/primitives/widgets/NumberInput.svelte.test.js +48 -0
  53. package/dist/primitives/widgets/PickerList.d.ts +24 -0
  54. package/dist/primitives/widgets/PickerList.js +21 -0
  55. package/dist/primitives/widgets/PickerList.svelte +150 -0
  56. package/dist/primitives/widgets/PickerList.svelte.d.ts +16 -0
  57. package/dist/primitives/widgets/PickerList.svelte.test.d.ts +1 -0
  58. package/dist/primitives/widgets/PickerList.svelte.test.js +31 -0
  59. package/dist/primitives/widgets/PickerList.test.d.ts +1 -0
  60. package/dist/primitives/widgets/PickerList.test.js +218 -0
  61. package/dist/primitives/widgets/RangeSlider.svelte +11 -4
  62. package/dist/primitives/widgets/RangeSlider.svelte.d.ts +2 -2
  63. package/dist/primitives/widgets/RangeSlider.svelte.test.d.ts +1 -0
  64. package/dist/primitives/widgets/RangeSlider.svelte.test.js +38 -0
  65. package/dist/primitives/widgets/Segmented.svelte +4 -4
  66. package/dist/primitives/widgets/Segmented.svelte.d.ts +3 -3
  67. package/dist/primitives/widgets/Segmented.svelte.test.d.ts +1 -0
  68. package/dist/primitives/widgets/Segmented.svelte.test.js +25 -0
  69. package/dist/primitives/widgets/Select.svelte +4 -4
  70. package/dist/primitives/widgets/Select.svelte.d.ts +3 -3
  71. package/dist/primitives/widgets/Select.svelte.test.d.ts +1 -0
  72. package/dist/primitives/widgets/Select.svelte.test.js +37 -0
  73. package/dist/primitives/widgets/Slider.svelte +4 -2
  74. package/dist/primitives/widgets/Slider.svelte.d.ts +2 -2
  75. package/dist/primitives/widgets/Slider.svelte.test.d.ts +1 -0
  76. package/dist/primitives/widgets/Slider.svelte.test.js +22 -0
  77. package/dist/primitives/widgets/SliderGroup.svelte +4 -2
  78. package/dist/primitives/widgets/SliderGroup.svelte.d.ts +2 -2
  79. package/dist/primitives/widgets/SliderGroup.svelte.test.d.ts +1 -0
  80. package/dist/primitives/widgets/SliderGroup.svelte.test.js +34 -0
  81. package/dist/primitives/widgets/Textarea.svelte +5 -2
  82. package/dist/primitives/widgets/Textarea.svelte.d.ts +2 -2
  83. package/dist/primitives/widgets/Textarea.svelte.test.d.ts +1 -0
  84. package/dist/primitives/widgets/Textarea.svelte.test.js +29 -0
  85. package/dist/primitives/widgets/UserPicker.svelte +53 -0
  86. package/dist/primitives/widgets/UserPicker.svelte.d.ts +9 -0
  87. package/dist/primitives/widgets/UserPicker.svelte.test.d.ts +1 -0
  88. package/dist/primitives/widgets/UserPicker.svelte.test.js +30 -0
  89. package/dist/primitives/widgets/UserPicker.test.d.ts +1 -0
  90. package/dist/primitives/widgets/UserPicker.test.js +115 -0
  91. package/dist/primitives/widgets/_contract.d.ts +27 -0
  92. package/dist/primitives/widgets/_contract.js +10 -0
  93. package/dist/projects/session-state.svelte.d.ts +17 -0
  94. package/dist/projects/session-state.svelte.js +39 -0
  95. package/dist/projects/session-state.test.d.ts +1 -0
  96. package/dist/projects/session-state.test.js +55 -0
  97. package/dist/projects-shard/DeleteProjectDialog.svelte +150 -0
  98. package/dist/projects-shard/DeleteProjectDialog.svelte.d.ts +12 -0
  99. package/dist/projects-shard/DeleteProjectDialog.test.d.ts +1 -0
  100. package/dist/projects-shard/DeleteProjectDialog.test.js +120 -0
  101. package/dist/projects-shard/ProjectManage.svelte +209 -0
  102. package/dist/projects-shard/ProjectManage.svelte.d.ts +8 -0
  103. package/dist/projects-shard/ProjectsSection.svelte +120 -0
  104. package/dist/projects-shard/ProjectsSection.svelte.d.ts +3 -0
  105. package/dist/projects-shard/index.d.ts +4 -0
  106. package/dist/projects-shard/index.js +4 -0
  107. package/dist/projects-shard/projectsApi.d.ts +20 -0
  108. package/dist/projects-shard/projectsApi.js +44 -0
  109. package/dist/projects-shard/projectsApi.test.d.ts +1 -0
  110. package/dist/projects-shard/projectsApi.test.js +71 -0
  111. package/dist/projects-shard/projectsShard.svelte.d.ts +10 -0
  112. package/dist/projects-shard/projectsShard.svelte.js +148 -0
  113. package/dist/sh3core-shard/ShellHome.svelte +19 -1
  114. package/dist/shards/activate-scopeid.test.d.ts +1 -0
  115. package/dist/shards/{activate-tenantid.test.js → activate-scopeid.test.js} +6 -6
  116. package/dist/version.d.ts +1 -1
  117. package/dist/version.js +1 -1
  118. package/package.json +1 -1
  119. /package/dist/{shards/activate-tenantid.test.d.ts → apps/workspace-rekey.test.d.ts} +0 -0
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { LiveInputEvents } from './_contract';
2
3
  import { valueToPercent } from './Slider';
3
4
  import { constrainPair, type Pair } from './RangeSlider';
4
5
 
@@ -10,6 +11,7 @@
10
11
  disabled = false,
11
12
  invalid = false,
12
13
  size = 'md',
14
+ oninput,
13
15
  onchange,
14
16
  }: {
15
17
  value?: Pair;
@@ -19,14 +21,19 @@
19
21
  disabled?: boolean;
20
22
  invalid?: boolean;
21
23
  size?: 'sm' | 'md';
22
- onchange?: (next: Pair) => void;
23
- } = $props();
24
+ } & LiveInputEvents<Pair> = $props();
24
25
 
25
26
  const lowPct = $derived(valueToPercent(value[0], min, max));
26
27
  const highPct = $derived(valueToPercent(value[1], min, max));
27
28
 
28
- function setLow(n: number) { value = constrainPair(value, 'low', n); }
29
- function setHigh(n: number) { value = constrainPair(value, 'high', n); }
29
+ function setLow(n: number) {
30
+ value = constrainPair(value, 'low', n);
31
+ oninput?.(value);
32
+ }
33
+ function setHigh(n: number) {
34
+ value = constrainPair(value, 'high', n);
35
+ oninput?.(value);
36
+ }
30
37
  </script>
31
38
 
32
39
  <div class="sh3-range" class:sh3-range--sm={size === 'sm'} class:sh3-range--invalid={invalid}>
@@ -1,3 +1,4 @@
1
+ import type { LiveInputEvents } from './_contract';
1
2
  import { type Pair } from './RangeSlider';
2
3
  type $$ComponentProps = {
3
4
  value?: Pair;
@@ -7,8 +8,7 @@ type $$ComponentProps = {
7
8
  disabled?: boolean;
8
9
  invalid?: boolean;
9
10
  size?: 'sm' | 'md';
10
- onchange?: (next: Pair) => void;
11
- };
11
+ } & LiveInputEvents<Pair>;
12
12
  declare const RangeSlider: import("svelte").Component<$$ComponentProps, {}, "value">;
13
13
  type RangeSlider = ReturnType<typeof RangeSlider>;
14
14
  export default RangeSlider;
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import RangeSlider from './RangeSlider.svelte';
4
+ describe('RangeSlider event contract', () => {
5
+ it('fires oninput per thumb with constrained pair', async () => {
6
+ const oninput = vi.fn();
7
+ const { container } = render(RangeSlider, {
8
+ props: { value: [10, 90], min: 0, max: 100, oninput },
9
+ });
10
+ const lows = container.querySelectorAll('input[type=range]');
11
+ const low = lows[0];
12
+ await fireEvent.input(low, { target: { value: '20' } });
13
+ expect(oninput).toHaveBeenCalledTimes(1);
14
+ const lastCall = oninput.mock.calls[oninput.mock.calls.length - 1][0];
15
+ expect(lastCall).toEqual([20, 90]);
16
+ });
17
+ it('fires onchange per thumb on change event', async () => {
18
+ const onchange = vi.fn();
19
+ const { container } = render(RangeSlider, {
20
+ props: { value: [10, 90], min: 0, max: 100, onchange },
21
+ });
22
+ const high = container.querySelectorAll('input[type=range]')[1];
23
+ await fireEvent.input(high, { target: { value: '80' } });
24
+ await fireEvent.change(high, { target: { value: '80' } });
25
+ expect(onchange).toHaveBeenCalledTimes(1);
26
+ expect(onchange.mock.calls[0][0]).toEqual([10, 80]);
27
+ });
28
+ it('thumbs cannot cross', async () => {
29
+ const oninput = vi.fn();
30
+ const { container } = render(RangeSlider, {
31
+ props: { value: [10, 90], min: 0, max: 100, oninput },
32
+ });
33
+ const low = container.querySelectorAll('input[type=range]')[0];
34
+ await fireEvent.input(low, { target: { value: '99' } });
35
+ const lastCall = oninput.mock.calls[oninput.mock.calls.length - 1][0];
36
+ expect(lastCall[0]).toBeLessThanOrEqual(lastCall[1]);
37
+ });
38
+ });
@@ -1,19 +1,19 @@
1
1
  <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
2
3
  import { type SegmentedOption, nextValue, prevValue, firstValue, lastValue } from './Segmented';
3
4
 
4
5
  let {
5
6
  options,
6
- value = $bindable(),
7
+ value = $bindable(options[0]?.value ?? ''),
7
8
  size = 'md',
8
9
  disabled = false,
9
10
  onchange,
10
11
  }: {
11
12
  options: SegmentedOption[];
12
- value: string;
13
+ value?: string;
13
14
  size?: 'sm' | 'md';
14
15
  disabled?: boolean;
15
- onchange?: (next: string) => void;
16
- } = $props();
16
+ } & CommitOnlyEvents<string> = $props();
17
17
 
18
18
  function commit(next: string) {
19
19
  value = next;
@@ -1,11 +1,11 @@
1
+ import type { CommitOnlyEvents } from './_contract';
1
2
  import { type SegmentedOption } from './Segmented';
2
3
  type $$ComponentProps = {
3
4
  options: SegmentedOption[];
4
- value: string;
5
+ value?: string;
5
6
  size?: 'sm' | 'md';
6
7
  disabled?: boolean;
7
- onchange?: (next: string) => void;
8
- };
8
+ } & CommitOnlyEvents<string>;
9
9
  declare const Segmented: import("svelte").Component<$$ComponentProps, {}, "value">;
10
10
  type Segmented = ReturnType<typeof Segmented>;
11
11
  export default Segmented;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import Segmented from './Segmented.svelte';
4
+ describe('Segmented event contract', () => {
5
+ const options = [
6
+ { value: 'a', label: 'A' },
7
+ { value: 'b', label: 'B' },
8
+ { value: 'c', label: 'C' },
9
+ ];
10
+ it('fires onchange once on click', async () => {
11
+ const onchange = vi.fn();
12
+ const { getByText } = render(Segmented, { props: { options, value: 'a', onchange } });
13
+ await fireEvent.click(getByText('B'));
14
+ expect(onchange).toHaveBeenCalledTimes(1);
15
+ expect(onchange).toHaveBeenCalledWith('b');
16
+ });
17
+ it('defaults value to first option when not passed', () => {
18
+ const { container } = render(Segmented, { props: { options } });
19
+ const active = container.querySelector('.sh3-seg__btn--active');
20
+ expect(active === null || active === void 0 ? void 0 : active.textContent).toBe('A');
21
+ });
22
+ it('does not throw props_invalid_value when bound to an undefined source', () => {
23
+ expect(() => render(Segmented, { props: { options, value: undefined } })).not.toThrow();
24
+ });
25
+ });
@@ -1,13 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
+ import type { CommitOnlyEvents } from './_contract';
3
4
  import { shell } from '../../shellRuntime.svelte';
4
5
  import Listbox from './_select-listbox.svelte';
5
6
  import { type SelectOption, resolveValueLabel, toggleMulti } from './Select';
6
7
 
7
8
  let {
8
9
  options,
9
- value = $bindable<string | string[]>(''),
10
10
  multiple = false,
11
+ value = $bindable<string | string[]>(multiple ? [] : ''),
11
12
  placeholder = 'Select…',
12
13
  label,
13
14
  disabled = false,
@@ -16,15 +17,14 @@
16
17
  onchange,
17
18
  }: {
18
19
  options: SelectOption[];
19
- value?: string | string[];
20
20
  multiple?: boolean;
21
+ value?: string | string[];
21
22
  placeholder?: string;
22
23
  label?: string;
23
24
  disabled?: boolean;
24
25
  invalid?: boolean;
25
26
  size?: 'sm' | 'md';
26
- onchange?: (next: string | string[]) => void;
27
- } = $props();
27
+ } & CommitOnlyEvents<string | string[]> = $props();
28
28
 
29
29
  let trigger = $state<HTMLButtonElement | undefined>(undefined);
30
30
  let nativeRef = $state<HTMLSelectElement | undefined>(undefined);
@@ -1,15 +1,15 @@
1
+ import type { CommitOnlyEvents } from './_contract';
1
2
  import { type SelectOption } from './Select';
2
3
  type $$ComponentProps = {
3
4
  options: SelectOption[];
4
- value?: string | string[];
5
5
  multiple?: boolean;
6
+ value?: string | string[];
6
7
  placeholder?: string;
7
8
  label?: string;
8
9
  disabled?: boolean;
9
10
  invalid?: boolean;
10
11
  size?: 'sm' | 'md';
11
- onchange?: (next: string | string[]) => void;
12
- };
12
+ } & CommitOnlyEvents<string | string[]>;
13
13
  declare const Select: import("svelte").Component<$$ComponentProps, {}, "value">;
14
14
  type Select = ReturnType<typeof Select>;
15
15
  export default Select;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import Select from './Select.svelte';
4
+ vi.mock('../../shellRuntime.svelte', () => ({
5
+ shell: { popup: { show: () => ({ close: () => { } }) } },
6
+ }));
7
+ describe('Select event contract', () => {
8
+ const options = [
9
+ { value: 'a', label: 'Apple' },
10
+ { value: 'b', label: 'Banana' },
11
+ { value: 'c', label: 'Cherry' },
12
+ ];
13
+ it('fires onchange via native select in single mode', async () => {
14
+ const onchange = vi.fn();
15
+ const { container } = render(Select, { props: { options, value: 'a', onchange } });
16
+ const native = container.querySelector('select');
17
+ native.value = 'c';
18
+ await fireEvent.change(native);
19
+ expect(onchange).toHaveBeenCalledTimes(1);
20
+ expect(onchange).toHaveBeenCalledWith('c');
21
+ });
22
+ it('defaults to empty array in multi mode', () => {
23
+ const { container } = render(Select, { props: { options, multiple: true } });
24
+ const native = container.querySelector('select');
25
+ // No <option> should carry a `selected` attribute when value defaults to [].
26
+ // (happy-dom may surface a selectedOptions item for the first option in a
27
+ // multi-select that has no explicit selection — assert the attribute the
28
+ // template controls instead.)
29
+ const optionsEls = Array.from(native.querySelectorAll('option'));
30
+ expect(optionsEls.every((o) => !o.hasAttribute('selected'))).toBe(true);
31
+ });
32
+ it('defaults to empty string in single mode', () => {
33
+ const { container } = render(Select, { props: { options } });
34
+ const trigger = container.querySelector('.sh3-select__display');
35
+ expect(trigger === null || trigger === void 0 ? void 0 : trigger.classList.contains('sh3-select__display--placeholder')).toBe(true);
36
+ });
37
+ });
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { LiveInputEvents } from './_contract';
2
3
  import { valueToPercent } from './Slider';
3
4
 
4
5
  let {
@@ -12,6 +13,7 @@
12
13
  invalid = false,
13
14
  size = 'md',
14
15
  orientation = 'horizontal',
16
+ oninput,
15
17
  onchange,
16
18
  }: {
17
19
  value?: number;
@@ -24,8 +26,7 @@
24
26
  invalid?: boolean;
25
27
  size?: 'sm' | 'md';
26
28
  orientation?: 'horizontal' | 'vertical';
27
- onchange?: (next: number) => void;
28
- } = $props();
29
+ } & LiveInputEvents<number> = $props();
29
30
 
30
31
  const pct = $derived(valueToPercent(value, min, max));
31
32
  </script>
@@ -50,6 +51,7 @@
50
51
  {disabled}
51
52
  aria-invalid={invalid || undefined}
52
53
  bind:value
54
+ oninput={() => oninput?.(value)}
53
55
  onchange={() => onchange?.(value)}
54
56
  />
55
57
  {#if showValue}
@@ -1,3 +1,4 @@
1
+ import type { LiveInputEvents } from './_contract';
1
2
  type $$ComponentProps = {
2
3
  value?: number;
3
4
  min?: number;
@@ -9,8 +10,7 @@ type $$ComponentProps = {
9
10
  invalid?: boolean;
10
11
  size?: 'sm' | 'md';
11
12
  orientation?: 'horizontal' | 'vertical';
12
- onchange?: (next: number) => void;
13
- };
13
+ } & LiveInputEvents<number>;
14
14
  declare const Slider: import("svelte").Component<$$ComponentProps, {}, "value">;
15
15
  type Slider = ReturnType<typeof Slider>;
16
16
  export default Slider;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import Slider from './Slider.svelte';
4
+ describe('Slider event contract', () => {
5
+ it('fires oninput on native input event', async () => {
6
+ const oninput = vi.fn();
7
+ const { container } = render(Slider, { props: { value: 0, min: 0, max: 100, oninput } });
8
+ const range = container.querySelector('input[type=range]');
9
+ await fireEvent.input(range, { target: { value: '42' } });
10
+ expect(oninput).toHaveBeenCalledTimes(1);
11
+ expect(oninput).toHaveBeenCalledWith(42);
12
+ });
13
+ it('fires onchange on native change event', async () => {
14
+ const onchange = vi.fn();
15
+ const { container } = render(Slider, { props: { value: 0, min: 0, max: 100, onchange } });
16
+ const range = container.querySelector('input[type=range]');
17
+ await fireEvent.input(range, { target: { value: '70' } });
18
+ await fireEvent.change(range, { target: { value: '70' } });
19
+ expect(onchange).toHaveBeenCalledTimes(1);
20
+ expect(onchange).toHaveBeenCalledWith(70);
21
+ });
22
+ });
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { LiveInputEvents } from './_contract';
2
3
  import Slider from './Slider.svelte';
3
4
 
4
5
  type Channel = { id: string; label: string; min?: number; max?: number; step?: number };
@@ -10,6 +11,7 @@
10
11
  showValues = false,
11
12
  disabled = false,
12
13
  size = 'md',
14
+ oninput,
13
15
  onchange,
14
16
  }: {
15
17
  orientation?: 'horizontal' | 'vertical';
@@ -18,8 +20,7 @@
18
20
  showValues?: boolean;
19
21
  disabled?: boolean;
20
22
  size?: 'sm' | 'md';
21
- onchange?: (next: Record<string, number>) => void;
22
- } = $props();
23
+ } & LiveInputEvents<Record<string, number>> = $props();
23
24
  </script>
24
25
 
25
26
  <div class="sh3-sg sh3-sg--{orientation}">
@@ -38,6 +39,7 @@
38
39
  () => values[ch.id] ?? 0,
39
40
  (n: number) => values = { ...values, [ch.id]: n }
40
41
  }
42
+ oninput={() => oninput?.(values)}
41
43
  onchange={() => onchange?.(values)}
42
44
  />
43
45
  </div>
@@ -1,3 +1,4 @@
1
+ import type { LiveInputEvents } from './_contract';
1
2
  type Channel = {
2
3
  id: string;
3
4
  label: string;
@@ -12,8 +13,7 @@ type $$ComponentProps = {
12
13
  showValues?: boolean;
13
14
  disabled?: boolean;
14
15
  size?: 'sm' | 'md';
15
- onchange?: (next: Record<string, number>) => void;
16
- };
16
+ } & LiveInputEvents<Record<string, number>>;
17
17
  declare const SliderGroup: import("svelte").Component<$$ComponentProps, {}, "values">;
18
18
  type SliderGroup = ReturnType<typeof SliderGroup>;
19
19
  export default SliderGroup;
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import SliderGroup from './SliderGroup.svelte';
4
+ describe('SliderGroup event contract', () => {
5
+ const channels = [
6
+ { id: 'r', label: 'R' },
7
+ { id: 'g', label: 'G' },
8
+ { id: 'b', label: 'B' },
9
+ ];
10
+ it('emits oninput with full record when a child slider moves', async () => {
11
+ const oninput = vi.fn();
12
+ const { container } = render(SliderGroup, {
13
+ props: { channels, values: { r: 0, g: 0, b: 0 }, oninput },
14
+ });
15
+ const ranges = container.querySelectorAll('input[type=range]');
16
+ const gSlider = ranges[1];
17
+ await fireEvent.input(gSlider, { target: { value: '50' } });
18
+ expect(oninput).toHaveBeenCalled();
19
+ const lastCall = oninput.mock.calls[oninput.mock.calls.length - 1][0];
20
+ expect(lastCall).toEqual({ r: 0, g: 50, b: 0 });
21
+ });
22
+ it('emits onchange with full record on commit', async () => {
23
+ const onchange = vi.fn();
24
+ const { container } = render(SliderGroup, {
25
+ props: { channels, values: { r: 0, g: 0, b: 0 }, onchange },
26
+ });
27
+ const bSlider = container.querySelectorAll('input[type=range]')[2];
28
+ await fireEvent.input(bSlider, { target: { value: '75' } });
29
+ await fireEvent.change(bSlider, { target: { value: '75' } });
30
+ expect(onchange).toHaveBeenCalled();
31
+ const lastCall = onchange.mock.calls[onchange.mock.calls.length - 1][0];
32
+ expect(lastCall).toEqual({ r: 0, g: 0, b: 75 });
33
+ });
34
+ });
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import type { LiveInputEvents } from './_contract';
3
+
2
4
  let {
3
5
  value = $bindable(''),
4
6
  label,
@@ -11,6 +13,7 @@
11
13
  required = false,
12
14
  rows = 3,
13
15
  resize = 'vertical',
16
+ oninput,
14
17
  onchange,
15
18
  }: {
16
19
  value?: string;
@@ -24,8 +27,7 @@
24
27
  required?: boolean;
25
28
  rows?: number;
26
29
  resize?: 'none' | 'vertical' | 'both';
27
- onchange?: (next: string) => void;
28
- } = $props();
30
+ } & LiveInputEvents<string> = $props();
29
31
 
30
32
  const showError = $derived(invalid && !!error);
31
33
  const helperText = $derived(showError ? error : helper);
@@ -42,6 +44,7 @@
42
44
  {rows}
43
45
  aria-invalid={invalid || undefined}
44
46
  bind:value
47
+ oninput={() => oninput?.(value)}
45
48
  onblur={() => onchange?.(value)}
46
49
  ></textarea>
47
50
  {#if helperText}<span class="sh3-textarea__helper" class:sh3-textarea__helper--error={showError}>{helperText}</span>{/if}
@@ -1,3 +1,4 @@
1
+ import type { LiveInputEvents } from './_contract';
1
2
  type $$ComponentProps = {
2
3
  value?: string;
3
4
  label?: string;
@@ -10,8 +11,7 @@ type $$ComponentProps = {
10
11
  required?: boolean;
11
12
  rows?: number;
12
13
  resize?: 'none' | 'vertical' | 'both';
13
- onchange?: (next: string) => void;
14
- };
14
+ } & LiveInputEvents<string>;
15
15
  declare const Textarea: import("svelte").Component<$$ComponentProps, {}, "value">;
16
16
  type Textarea = ReturnType<typeof Textarea>;
17
17
  export default Textarea;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import Textarea from './Textarea.svelte';
4
+ describe('Textarea event contract', () => {
5
+ it('fires oninput on every keystroke', async () => {
6
+ const oninput = vi.fn();
7
+ const { container } = render(Textarea, { props: { value: '', oninput } });
8
+ const ta = container.querySelector('textarea');
9
+ await fireEvent.input(ta, { target: { value: 'a' } });
10
+ await fireEvent.input(ta, { target: { value: 'ab' } });
11
+ expect(oninput).toHaveBeenCalledTimes(2);
12
+ expect(oninput).toHaveBeenNthCalledWith(2, 'ab');
13
+ });
14
+ it('fires onchange once on blur with final value', async () => {
15
+ const onchange = vi.fn();
16
+ const { container } = render(Textarea, { props: { value: '', onchange } });
17
+ const ta = container.querySelector('textarea');
18
+ await fireEvent.input(ta, { target: { value: 'done' } });
19
+ await fireEvent.blur(ta);
20
+ expect(onchange).toHaveBeenCalledTimes(1);
21
+ expect(onchange).toHaveBeenCalledWith('done');
22
+ });
23
+ it('propagates rows and resize props', () => {
24
+ const { container } = render(Textarea, { props: { value: '', rows: 5, resize: 'none' } });
25
+ const ta = container.querySelector('textarea');
26
+ expect(ta.getAttribute('rows')).toBe('5');
27
+ expect(ta.style.resize).toBe('none');
28
+ });
29
+ });
@@ -0,0 +1,53 @@
1
+ <script lang="ts">
2
+ import type { CommitOnlyEvents } from './_contract';
3
+ import PickerList from './PickerList.svelte';
4
+ import type { PickerItem } from './PickerList';
5
+ import { usersAdminState, refreshAdminUsers } from '../../auth/admin-users.svelte';
6
+
7
+ let {
8
+ value = $bindable<string[]>([]),
9
+ onchange,
10
+ disabled = false,
11
+ size = 'md',
12
+ }: {
13
+ value?: string[];
14
+ disabled?: boolean;
15
+ size?: 'sm' | 'md';
16
+ } & CommitOnlyEvents<string[]> = $props();
17
+
18
+ // Kick a fetch if the cache is empty. The store dedupes concurrent calls,
19
+ // so multiple UserPicker mounts share one request. After completion
20
+ // subsequent mounts see the cached state.
21
+ if (
22
+ usersAdminState.users.length === 0 &&
23
+ !usersAdminState.loading &&
24
+ !usersAdminState.error
25
+ ) {
26
+ void refreshAdminUsers();
27
+ }
28
+
29
+ const items = $derived<PickerItem[]>(
30
+ usersAdminState.users.map((u) => ({
31
+ id: u.id,
32
+ label: u.displayName || u.username,
33
+ sublabel: u.username,
34
+ })),
35
+ );
36
+
37
+ function handleChange(next: string[]) {
38
+ value = next;
39
+ onchange?.(next);
40
+ }
41
+ </script>
42
+
43
+ <PickerList
44
+ {items}
45
+ {value}
46
+ onchange={handleChange}
47
+ loading={usersAdminState.loading}
48
+ error={usersAdminState.error}
49
+ onRetry={() => void refreshAdminUsers()}
50
+ emptyText="No users found."
51
+ {disabled}
52
+ {size}
53
+ />
@@ -0,0 +1,9 @@
1
+ import type { CommitOnlyEvents } from './_contract';
2
+ type $$ComponentProps = {
3
+ value?: string[];
4
+ disabled?: boolean;
5
+ size?: 'sm' | 'md';
6
+ } & CommitOnlyEvents<string[]>;
7
+ declare const UserPicker: import("svelte").Component<$$ComponentProps, {}, "value">;
8
+ type UserPicker = ReturnType<typeof UserPicker>;
9
+ export default UserPicker;
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, fireEvent } from '@testing-library/svelte';
3
+ import UserPicker from './UserPicker.svelte';
4
+ const refresh = vi.fn();
5
+ vi.mock('../../auth/admin-users.svelte', () => ({
6
+ usersAdminState: {
7
+ users: [
8
+ { id: 'u1', displayName: 'Alice', username: 'alice' },
9
+ { id: 'u2', displayName: '', username: 'bob' },
10
+ ],
11
+ loading: false,
12
+ error: null,
13
+ },
14
+ refreshAdminUsers: () => refresh(),
15
+ }));
16
+ describe('UserPicker event contract', () => {
17
+ it('renders cached users with displayName fallback to username', () => {
18
+ const { container } = render(UserPicker, { props: { value: [] } });
19
+ const labels = Array.from(container.querySelectorAll('.sh3-picker__row-label')).map((el) => el.textContent);
20
+ expect(labels).toEqual(['Alice', 'bob']);
21
+ });
22
+ it('fires onchange with id array on toggle', async () => {
23
+ const onchange = vi.fn();
24
+ const { container } = render(UserPicker, { props: { value: [], onchange } });
25
+ const checkboxes = container.querySelectorAll('input[type=checkbox]');
26
+ await fireEvent.click(checkboxes[0]);
27
+ expect(onchange).toHaveBeenCalledTimes(1);
28
+ expect(onchange).toHaveBeenCalledWith(['u1']);
29
+ });
30
+ });
@@ -0,0 +1 @@
1
+ export {};