@streamscloud/kit 0.1.8 → 0.1.10

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 (49) hide show
  1. package/dist/core/toastr/toastr.scss +2 -1
  2. package/dist/core/utils/color-helper.d.ts +13 -0
  3. package/dist/core/utils/color-helper.js +39 -0
  4. package/dist/core/utils/index.d.ts +1 -0
  5. package/dist/core/utils/index.js +1 -0
  6. package/dist/ui/color-picker/cmp.color-picker.svelte +150 -0
  7. package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +33 -0
  8. package/dist/ui/color-picker/cmp.input-stub.svelte +98 -0
  9. package/dist/ui/color-picker/cmp.input-stub.svelte.d.ts +40 -0
  10. package/dist/ui/color-picker/color-picker-localization.d.ts +3 -0
  11. package/dist/ui/color-picker/color-picker-localization.js +12 -0
  12. package/dist/ui/color-picker/index.d.ts +1 -0
  13. package/dist/ui/color-picker/index.js +1 -0
  14. package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +109 -0
  15. package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte.d.ts +9 -0
  16. package/dist/ui/cropper/image-editor-dialog/image-editor-dialog-localization.d.ts +6 -0
  17. package/dist/ui/cropper/image-editor-dialog/image-editor-dialog-localization.js +33 -0
  18. package/dist/ui/cropper/image-editor-dialog/index.d.ts +21 -0
  19. package/dist/ui/cropper/image-editor-dialog/index.js +25 -0
  20. package/dist/ui/cropper/image-editor-dialog/types.d.ts +25 -0
  21. package/dist/ui/cropper/image-editor-dialog/types.js +1 -0
  22. package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +67 -0
  23. package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +21 -0
  24. package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +228 -0
  25. package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +28 -0
  26. package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +198 -0
  27. package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +58 -0
  28. package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +33 -0
  29. package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +40 -0
  30. package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +159 -0
  31. package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +40 -0
  32. package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +163 -0
  33. package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +6 -0
  34. package/dist/ui/cropper/img-cropper/img-cropper-localization.js +33 -0
  35. package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +11 -0
  36. package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +68 -0
  37. package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +32 -0
  38. package/dist/ui/cropper/img-cropper/img-cropper-utils.js +138 -0
  39. package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +39 -0
  40. package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +1 -0
  41. package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +81 -0
  42. package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +160 -0
  43. package/dist/ui/cropper/img-cropper/index.d.ts +4 -0
  44. package/dist/ui/cropper/img-cropper/index.js +4 -0
  45. package/dist/ui/icon-text/cmp.icon-text.svelte +90 -0
  46. package/dist/ui/icon-text/cmp.icon-text.svelte.d.ts +39 -0
  47. package/dist/ui/icon-text/index.d.ts +1 -0
  48. package/dist/ui/icon-text/index.js +1 -0
  49. package/package.json +27 -5
@@ -1,5 +1,6 @@
1
- .sc-toast[data-sonner-toast] {
1
+ .sc-toast[data-sonner-toast][data-styled='true'] {
2
2
  font-family: inherit;
3
+ font-size: 14px;
3
4
  --toast-close-button-start: auto;
4
5
  --toast-close-button-end: 0;
5
6
  --toast-close-button-transform: translate(35%, -35%);
@@ -0,0 +1,13 @@
1
+ export declare class ColorHelper {
2
+ /**
3
+ * Normalize a color value to a clean hex string.
4
+ * - Strips 100% alpha (`#FF0000FF` → `#FF0000`)
5
+ * - Returns `''` for transparent / empty / invalid values
6
+ * - Preserves partial alpha (`#FF000080` stays as-is)
7
+ */
8
+ static normalizeHex(value: string | null | undefined): string;
9
+ /**
10
+ * Check whether a color value represents "no color" (empty, transparent, zero alpha).
11
+ */
12
+ static isTransparent(value: string | null | undefined): boolean;
13
+ }
@@ -0,0 +1,39 @@
1
+ import { colord, extend } from 'colord';
2
+ import namesPlugin from 'colord/plugins/names';
3
+ extend([namesPlugin]);
4
+ export class ColorHelper {
5
+ /**
6
+ * Normalize a color value to a clean hex string.
7
+ * - Strips 100% alpha (`#FF0000FF` → `#FF0000`)
8
+ * - Returns `''` for transparent / empty / invalid values
9
+ * - Preserves partial alpha (`#FF000080` stays as-is)
10
+ */
11
+ static normalizeHex(value) {
12
+ if (!value) {
13
+ return '';
14
+ }
15
+ const parsed = colord(value);
16
+ if (!parsed.isValid()) {
17
+ return '';
18
+ }
19
+ const alpha = parsed.alpha();
20
+ if (alpha === 0) {
21
+ return '';
22
+ }
23
+ const hex = parsed.toHex().toUpperCase();
24
+ if (alpha === 1) {
25
+ return hex.substring(0, 7);
26
+ }
27
+ return hex;
28
+ }
29
+ /**
30
+ * Check whether a color value represents "no color" (empty, transparent, zero alpha).
31
+ */
32
+ static isTransparent(value) {
33
+ if (!value || value === 'transparent') {
34
+ return true;
35
+ }
36
+ const parsed = colord(value);
37
+ return !parsed.isValid() || parsed.alpha() === 0;
38
+ }
39
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './array-helper';
2
+ export * from './color-helper';
2
3
  export * from './compact-number';
3
4
  export * from './base64-serializer';
4
5
  export * from './browser';
@@ -1,4 +1,5 @@
1
1
  export * from './array-helper';
2
+ export * from './color-helper';
2
3
  export * from './compact-number';
3
4
  export * from './base64-serializer';
4
5
  export * from './browser';
@@ -0,0 +1,150 @@
1
+ <script lang="ts">import { ColorHelper } from '../../core/utils';
2
+ import { Dropdown } from '../dropdown';
3
+ import { Icon } from '../icon';
4
+ import { default as InputStub } from './cmp.input-stub.svelte';
5
+ import { ColorPickerLocalization } from './color-picker-localization';
6
+ import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
7
+ import IconColorF from '@fluentui/svg-icons/icons/color_20_regular.svg?raw';
8
+ import IconDismiss from '@fluentui/svg-icons/icons/dismiss_20_regular.svg?raw';
9
+ import { colord } from 'colord';
10
+ import AwesomeColorPicker from 'svelte-awesome-color-picker';
11
+ let { value, enableReset = false, on, children, defaultPreviewIcon } = $props();
12
+ const loc = new ColorPickerLocalization();
13
+ const textColor = $derived.by(() => {
14
+ if (!value) {
15
+ return null;
16
+ }
17
+ else {
18
+ const hex = colord(value).toHex();
19
+ const color = hex.substring(1, 7);
20
+ const red = parseInt(color.substring(0, 2), 16);
21
+ const green = parseInt(color.substring(2, 4), 16);
22
+ const blue = parseInt(color.substring(4, 6), 16);
23
+ if (red * 0.299 + green * 0.587 + blue * 0.114 > 149) {
24
+ return '#000000';
25
+ }
26
+ else {
27
+ return '#ffffff';
28
+ }
29
+ }
30
+ });
31
+ const previewBackgroundColor = $derived.by(() => {
32
+ if (!value) {
33
+ return '';
34
+ }
35
+ else {
36
+ return colord(value).toHex().substring(0, 7);
37
+ }
38
+ });
39
+ const resetValue = (e) => {
40
+ e.preventDefault();
41
+ e.stopPropagation();
42
+ if (enableReset) {
43
+ on?.change?.('');
44
+ }
45
+ };
46
+ const onColorPickerInput = ({ hex }) => {
47
+ on?.change?.(ColorHelper.normalizeHex(hex));
48
+ };
49
+ </script>
50
+
51
+ <div class="color-picker">
52
+ <Dropdown position="bottom-start" keepDropdownOpen>
53
+ {#snippet trigger()}
54
+ <div class="color-picker__trigger">
55
+ {#if children}
56
+ {@render children()}
57
+ {:else}
58
+ <InputStub
59
+ value={value || ''}
60
+ on={{}}
61
+ inert={true}
62
+ placeholder={loc.notSet}
63
+ --sc-kit--input--background={previewBackgroundColor}
64
+ --sc-kit--input--text--color={textColor}
65
+ --sc-kit--input--icon--color={textColor}>
66
+ {#snippet icon()}
67
+ {#if defaultPreviewIcon}
68
+ {@render defaultPreviewIcon()}
69
+ {:else}
70
+ <Icon src={IconColorF} color={value ? null : 'gray'} />
71
+ {/if}
72
+ {/snippet}
73
+ {#snippet clearButton()}
74
+ {#if !value}
75
+ <button type="button" inert={true}>
76
+ <Icon src={IconChevronDown} color="gray" />
77
+ </button>
78
+ {:else}
79
+ <button type="button" inert={!enableReset} onclick={resetValue}>
80
+ {#if enableReset}
81
+ <Icon src={IconDismiss} />
82
+ {:else}
83
+ <Icon src={IconChevronDown} />
84
+ {/if}
85
+ </button>
86
+ {/if}
87
+ {/snippet}
88
+ </InputStub>
89
+ {/if}
90
+ </div>
91
+ {/snippet}
92
+
93
+ <div class="color-picker__panel">
94
+ <AwesomeColorPicker hex={value ? colord(value).toHex() : null} isDialog={false} onInput={onColorPickerInput} textInputModes={['hex']} />
95
+ </div>
96
+ </Dropdown>
97
+ </div>
98
+
99
+ <!--
100
+ @component
101
+ A color picker that displays a swatch preview trigger and opens a dropdown panel with a full color picker (powered by svelte-awesome-color-picker).
102
+
103
+ ### Props
104
+ | Prop | Type | Default | Description |
105
+ |---|---|---|---|
106
+ | `value` | `string \| null \| undefined` | — | Current color value (any CSS color format) |
107
+ | `enableReset` | `boolean` | `false` | Show dismiss icon to clear the value |
108
+ | `on.change` | `(value: string) => void` | — | Fires when color changes |
109
+ | `children` | `Snippet` | — | Custom trigger snippet (replaces default preview) |
110
+ | `defaultPreviewIcon` | `Snippet` | — | Custom icon in the default preview trigger |
111
+
112
+ ### CSS Custom Properties
113
+ | Property | Description | Default |
114
+ |---|---|---|
115
+ | `--sc-kit--color-picker--width` | Component width | `100%` |
116
+ | `--sc-kit--color-picker--default-preview--font-size` | Preview text font size | inherited |
117
+ | `--sc-kit--color-picker--default-preview--icon-size` | Preview icon size | `1rem` |
118
+ | `--sc-kit--color-picker--default-preview--height` | Preview trigger height | inherited |
119
+ -->
120
+
121
+ <style>.color-picker {
122
+ --sc-kit--dropdown--width: 100%;
123
+ --_color-picker--default-preview--font-size: var(--sc-kit--color-picker--default-preview--font-size);
124
+ --_color-picker--default-preview--icon-size: var(--sc-kit--color-picker--default-preview--icon-size, 1rem);
125
+ --_color-picker--default-preview--height: var(--sc-kit--color-picker--default-preview--height);
126
+ --_color-picker--width: var(--sc-kit--color-picker--width, 100%);
127
+ display: flex;
128
+ position: relative;
129
+ width: var(--_color-picker--width);
130
+ }
131
+ .color-picker__trigger {
132
+ width: 100%;
133
+ --sc-kit--input--text--font-size: var(--_color-picker--default-preview--font-size);
134
+ --sc-kit--input--icon--size: var(--_color-picker--default-preview--icon-size);
135
+ --sc-kit--input--height: var(--_color-picker--default-preview--height);
136
+ --sc-kit--input--cursor--inert: pointer;
137
+ }
138
+ .color-picker__panel {
139
+ --focus-color: none;
140
+ --cp-bg-color: light-dark(#ffffff, #1c1c1c);
141
+ --cp-border-color: light-dark(#d1d5db, #4b5563);
142
+ --cp-input-color: light-dark(#f2f2f3, #4b5563);
143
+ --cp-text-color: light-dark(#2e2e2e, #ffffff);
144
+ }
145
+ .color-picker__panel :global(.button-like) {
146
+ display: none;
147
+ }
148
+ .color-picker__panel :global(.wrapper.is-open) {
149
+ margin: 0;
150
+ }</style>
@@ -0,0 +1,33 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Props = {
3
+ value: string | null | undefined;
4
+ enableReset?: boolean;
5
+ on?: {
6
+ change?: (value: string) => void;
7
+ };
8
+ children?: Snippet;
9
+ defaultPreviewIcon?: Snippet;
10
+ };
11
+ /**
12
+ * A color picker that displays a swatch preview trigger and opens a dropdown panel with a full color picker (powered by svelte-awesome-color-picker).
13
+ *
14
+ * ### Props
15
+ * | Prop | Type | Default | Description |
16
+ * |---|---|---|---|
17
+ * | `value` | `string \| null \| undefined` | — | Current color value (any CSS color format) |
18
+ * | `enableReset` | `boolean` | `false` | Show dismiss icon to clear the value |
19
+ * | `on.change` | `(value: string) => void` | — | Fires when color changes |
20
+ * | `children` | `Snippet` | — | Custom trigger snippet (replaces default preview) |
21
+ * | `defaultPreviewIcon` | `Snippet` | — | Custom icon in the default preview trigger |
22
+ *
23
+ * ### CSS Custom Properties
24
+ * | Property | Description | Default |
25
+ * |---|---|---|
26
+ * | `--sc-kit--color-picker--width` | Component width | `100%` |
27
+ * | `--sc-kit--color-picker--default-preview--font-size` | Preview text font size | inherited |
28
+ * | `--sc-kit--color-picker--default-preview--icon-size` | Preview icon size | `1rem` |
29
+ * | `--sc-kit--color-picker--default-preview--height` | Preview trigger height | inherited |
30
+ */
31
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
32
+ type Cmp = ReturnType<typeof Cmp>;
33
+ export default Cmp;
@@ -0,0 +1,98 @@
1
+ <script lang="ts">import { untrack } from 'svelte';
2
+ // inert and on are accepted for Input-compatibility but unused in the stub
3
+ let { value, placeholder = '', inert, icon, clearButton, on } = $props();
4
+ untrack(() => void inert);
5
+ untrack(() => void on);
6
+ </script>
7
+
8
+ <div class="input-stub">
9
+ {#if icon}
10
+ <span class="input-stub__icon">
11
+ {@render icon()}
12
+ </span>
13
+ {/if}
14
+ <span class="input-stub__text" class:input-stub__text--placeholder={!value}>
15
+ {value || placeholder}
16
+ </span>
17
+ {#if clearButton}
18
+ <span class="input-stub__action">
19
+ {@render clearButton()}
20
+ </span>
21
+ {/if}
22
+ </div>
23
+
24
+ <!--
25
+ @component
26
+ Minimal input-field emulation for use inside ColorPicker.
27
+ Reads `--sc-kit--input--*` CSS variables set by the parent. Will be replaced by a real Input component when available.
28
+
29
+ ### Props
30
+ | Prop | Type | Default | Description |
31
+ |---|---|---|---|
32
+ | `value` | `string` | — | Displayed text |
33
+ | `placeholder` | `string` | `''` | Placeholder when value is empty |
34
+ | `inert` | `boolean` | — | Reserved for future Input compatibility |
35
+ | `on` | `Record<string, never>` | — | Reserved for future Input compatibility |
36
+ | `icon` | `Snippet` | — | Left icon slot |
37
+ | `clearButton` | `Snippet` | — | Right action slot |
38
+
39
+ ### CSS Custom Properties
40
+ | Property | Description | Default |
41
+ |---|---|---|
42
+ | `--sc-kit--input--height` | Container height | `2rem` |
43
+ | `--sc-kit--input--background` | Background color | light-dark white/dark |
44
+ | `--sc-kit--input--border-color` | Border color | light-dark neutral |
45
+ | `--sc-kit--input--border-radius` | Border radius | `0.25rem` |
46
+ | `--sc-kit--input--text--font-size` | Text font size | `0.875rem` |
47
+ | `--sc-kit--input--text--color` | Text color | light-dark gray/white |
48
+ | `--sc-kit--input--icon--size` | Icon size | `1rem` |
49
+ | `--sc-kit--input--icon--color` | Icon color | inherited |
50
+ | `--sc-kit--input--padding--hor` | Horizontal padding | `0.625rem` |
51
+ | `--sc-kit--input--cursor--inert` | Cursor when inert | `default` |
52
+ -->
53
+
54
+ <style>.input-stub {
55
+ --_input--height: var(--sc-kit--input--height, 2rem);
56
+ --_input--background: var(--sc-kit--input--background, light-dark(#ffffff, #222222));
57
+ --_input--border-color: var(--sc-kit--input--border-color, light-dark(#d1d5db, #383838));
58
+ --_input--border-radius: var(--sc-kit--input--border-radius, 0.25rem);
59
+ --_input--text-font-size: var(--sc-kit--input--text--font-size, 0.875rem);
60
+ --_input--text-color: var(--sc-kit--input--text--color, light-dark(#2e2e2e, #ffffff));
61
+ --_input--icon-size: var(--sc-kit--input--icon--size, 1rem);
62
+ --_input--icon-color: var(--sc-kit--input--icon--color);
63
+ --_input--padding-hor: var(--sc-kit--input--padding--hor, 0.625rem);
64
+ --_input--cursor: var(--sc-kit--input--cursor--inert, default);
65
+ display: flex;
66
+ align-items: center;
67
+ height: var(--_input--height);
68
+ background: var(--_input--background);
69
+ border: 1px solid var(--_input--border-color);
70
+ border-radius: var(--_input--border-radius);
71
+ padding-inline: var(--_input--padding-hor);
72
+ color: var(--_input--text-color);
73
+ cursor: var(--_input--cursor);
74
+ }
75
+ .input-stub__icon {
76
+ display: flex;
77
+ align-items: center;
78
+ flex-shrink: 0;
79
+ margin-right: 0.625rem;
80
+ --sc-kit--icon--size: var(--_input--icon-size);
81
+ --sc-kit--icon--color: var(--_input--icon-color);
82
+ }
83
+ .input-stub__text {
84
+ flex: 1;
85
+ overflow: hidden;
86
+ text-overflow: ellipsis;
87
+ white-space: nowrap;
88
+ font-size: var(--_input--text-font-size);
89
+ }
90
+ .input-stub__text--placeholder {
91
+ color: light-dark(#9ca3af, #6b7280);
92
+ }
93
+ .input-stub__action {
94
+ display: flex;
95
+ align-items: center;
96
+ flex-shrink: 0;
97
+ margin-left: auto;
98
+ }</style>
@@ -0,0 +1,40 @@
1
+ import { type Snippet } from 'svelte';
2
+ type Props = {
3
+ value: string;
4
+ placeholder?: string;
5
+ inert?: boolean;
6
+ on?: Record<string, never>;
7
+ icon?: Snippet;
8
+ clearButton?: Snippet;
9
+ };
10
+ /**
11
+ * Minimal input-field emulation for use inside ColorPicker.
12
+ * Reads `--sc-kit--input--*` CSS variables set by the parent. Will be replaced by a real Input component when available.
13
+ *
14
+ * ### Props
15
+ * | Prop | Type | Default | Description |
16
+ * |---|---|---|---|
17
+ * | `value` | `string` | — | Displayed text |
18
+ * | `placeholder` | `string` | `''` | Placeholder when value is empty |
19
+ * | `inert` | `boolean` | — | Reserved for future Input compatibility |
20
+ * | `on` | `Record<string, never>` | — | Reserved for future Input compatibility |
21
+ * | `icon` | `Snippet` | — | Left icon slot |
22
+ * | `clearButton` | `Snippet` | — | Right action slot |
23
+ *
24
+ * ### CSS Custom Properties
25
+ * | Property | Description | Default |
26
+ * |---|---|---|
27
+ * | `--sc-kit--input--height` | Container height | `2rem` |
28
+ * | `--sc-kit--input--background` | Background color | light-dark white/dark |
29
+ * | `--sc-kit--input--border-color` | Border color | light-dark neutral |
30
+ * | `--sc-kit--input--border-radius` | Border radius | `0.25rem` |
31
+ * | `--sc-kit--input--text--font-size` | Text font size | `0.875rem` |
32
+ * | `--sc-kit--input--text--color` | Text color | light-dark gray/white |
33
+ * | `--sc-kit--input--icon--size` | Icon size | `1rem` |
34
+ * | `--sc-kit--input--icon--color` | Icon color | inherited |
35
+ * | `--sc-kit--input--padding--hor` | Horizontal padding | `0.625rem` |
36
+ * | `--sc-kit--input--cursor--inert` | Cursor when inert | `default` |
37
+ */
38
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
39
+ type Cmp = ReturnType<typeof Cmp>;
40
+ export default Cmp;
@@ -0,0 +1,3 @@
1
+ export declare class ColorPickerLocalization {
2
+ get notSet(): string;
3
+ }
@@ -0,0 +1,12 @@
1
+ import { AppLocale } from '../../core/locale';
2
+ const loc = {
3
+ notSet: {
4
+ en: 'Not set',
5
+ no: 'Ikke angitt'
6
+ }
7
+ };
8
+ export class ColorPickerLocalization {
9
+ get notSet() {
10
+ return loc.notSet[AppLocale.current];
11
+ }
12
+ }
@@ -0,0 +1 @@
1
+ export { default as ColorPicker } from './cmp.color-picker.svelte';
@@ -0,0 +1 @@
1
+ export { default as ColorPicker } from './cmp.color-picker.svelte';
@@ -0,0 +1,109 @@
1
+ <script lang="ts">import { FileWithBlobDataHelper } from '../../../core/files';
2
+ import { Toastr } from '../../../core/toastr';
3
+ import { Dialog, DialogButton, DialogCancelButton, DialogSize } from '../../dialog';
4
+ import { ImgCropper, ImgCropperControls, ImgCropperToolbar, ImgCropperView } from '../img-cropper';
5
+ import { ImageEditorDialogLocalization } from './image-editor-dialog-localization';
6
+ import { untrack } from 'svelte';
7
+ const { controller, data } = $props();
8
+ const loc = new ImageEditorDialogLocalization();
9
+ const cropper = untrack(() => {
10
+ const showImageShadow = data.showImageShadow ?? true;
11
+ if (data.mode === 'contain') {
12
+ return new ImgCropper({ mode: 'contain', aspectRatio: data.aspectRatio, fillColor: data.fillColor, showImageShadow });
13
+ }
14
+ return new ImgCropper({ mode: 'cover', aspectRatio: data.aspectRatio, fillColor: data.fillColor, showImageShadow });
15
+ });
16
+ const url = $derived(data.url);
17
+ const save = async () => {
18
+ try {
19
+ const result = await cropper.save();
20
+ if (!result) {
21
+ Toastr.error(loc.saveError);
22
+ return;
23
+ }
24
+ const croppedFile = Object.assign(FileWithBlobDataHelper.fromBase64(result.dataUrl), {
25
+ width: result.width,
26
+ height: result.height
27
+ });
28
+ controller.ok({ croppedFile, selectedRatio: cropper.aspectRatio });
29
+ }
30
+ catch {
31
+ Toastr.error(loc.saveError);
32
+ }
33
+ };
34
+ const cancel = () => {
35
+ controller.cancel();
36
+ };
37
+ $effect(() => untrack(() => {
38
+ controller.updateSettings({ closeOnClickOutside: false, closeOnEsc: true });
39
+ controller.updateContainerSettings({ size: DialogSize.FullHD });
40
+ }));
41
+ </script>
42
+
43
+ <div class="image-editor-dialog">
44
+ <Dialog controller={controller}>
45
+ {#snippet title()}
46
+ {loc.title}
47
+ {/snippet}
48
+ {#snippet bodySection()}
49
+ <div class="image-editor-dialog__body">
50
+ <div class="image-editor-dialog__cropper-wrapper">
51
+ <ImgCropperView src={url} cropper={cropper} showControls={false} />
52
+ </div>
53
+ <div class="image-editor-dialog__controls">
54
+ <ImgCropperControls cropper={cropper} />
55
+ </div>
56
+ </div>
57
+ {/snippet}
58
+ {#snippet footer()}
59
+ <div class="image-editor-dialog__footer">
60
+ <div class="image-editor-dialog__toolbar">
61
+ <ImgCropperToolbar cropper={cropper} />
62
+ </div>
63
+ <div class="image-editor-dialog__buttons">
64
+ <DialogCancelButton on={{ click: cancel }}>{loc.cancel}</DialogCancelButton>
65
+ <DialogButton on={{ click: save }}>{loc.save}</DialogButton>
66
+ </div>
67
+ </div>
68
+ {/snippet}
69
+ </Dialog>
70
+ </div>
71
+
72
+ <style>.image-editor-dialog {
73
+ --sc-kit--dialog--height: calc(100vh - 2.5rem);
74
+ display: contents;
75
+ }
76
+ .image-editor-dialog__body {
77
+ flex: 1;
78
+ position: relative;
79
+ background: #DADADA;
80
+ overflow: hidden;
81
+ }
82
+ .image-editor-dialog__cropper-wrapper {
83
+ position: absolute;
84
+ inset: 3rem;
85
+ }
86
+ .image-editor-dialog__controls {
87
+ position: absolute;
88
+ bottom: 0.3125rem;
89
+ left: 50%;
90
+ transform: translateX(-50%);
91
+ z-index: 10;
92
+ }
93
+ .image-editor-dialog__footer {
94
+ container-type: inline-size;
95
+ display: flex;
96
+ flex: 1;
97
+ justify-content: space-between;
98
+ align-items: center;
99
+ }
100
+ .image-editor-dialog__toolbar {
101
+ flex: 1;
102
+ min-width: 0;
103
+ }
104
+ .image-editor-dialog__buttons {
105
+ white-space: nowrap;
106
+ display: flex;
107
+ gap: 1.5rem;
108
+ margin-left: 1rem;
109
+ }</style>
@@ -0,0 +1,9 @@
1
+ import { type DialogController } from '../../dialog';
2
+ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types';
3
+ type Props = {
4
+ controller: DialogController<ImageEditorDialogResult>;
5
+ data: ImageEditorDialogOptions;
6
+ };
7
+ declare const Cmp: import("svelte").Component<Props, {}, "">;
8
+ type Cmp = ReturnType<typeof Cmp>;
9
+ export default Cmp;
@@ -0,0 +1,6 @@
1
+ export declare class ImageEditorDialogLocalization {
2
+ get title(): string;
3
+ get cancel(): string;
4
+ get save(): string;
5
+ get saveError(): string;
6
+ }
@@ -0,0 +1,33 @@
1
+ import { AppLocale } from '../../../core/locale';
2
+ const loc = {
3
+ title: {
4
+ en: 'Edit Image',
5
+ no: 'Rediger bilde'
6
+ },
7
+ cancel: {
8
+ en: 'Cancel',
9
+ no: 'Avbryt'
10
+ },
11
+ save: {
12
+ en: 'Save',
13
+ no: 'Lagre'
14
+ },
15
+ saveError: {
16
+ en: 'Failed to save image',
17
+ no: 'Kunne ikke lagre bildet'
18
+ }
19
+ };
20
+ export class ImageEditorDialogLocalization {
21
+ get title() {
22
+ return loc.title[AppLocale.current];
23
+ }
24
+ get cancel() {
25
+ return loc.cancel[AppLocale.current];
26
+ }
27
+ get save() {
28
+ return loc.save[AppLocale.current];
29
+ }
30
+ get saveError() {
31
+ return loc.saveError[AppLocale.current];
32
+ }
33
+ }
@@ -0,0 +1,21 @@
1
+ import { type DialogResult } from '../../dialog';
2
+ import type { ImageEditorDialogOptions, ImageEditorDialogResult } from './types';
3
+ /**
4
+ * Opens a full-screen image editor dialog with crop, rotate, zoom, and reset controls.
5
+ *
6
+ * ### Modes
7
+ * - **`cover`** (default) — image fills the canvas; crop selects a visible region.
8
+ * - **`contain`** — image is fitted inside the canvas with optional fill color.
9
+ *
10
+ * ### Aspect ratio
11
+ * - `number` — fixed ratio, no dropdown.
12
+ * - `{ initial, supported }` (cover) — dropdown with supported ratios.
13
+ * - `{ initial?, supported, allowFreeCrop? }` (contain) — dropdown with optional free crop.
14
+ * - Omitted — free crop in contain, full canvas in cover.
15
+ *
16
+ * ### Result
17
+ * - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
18
+ * - `selectedRatio` — active aspect ratio at save time (`null` for free crop).
19
+ */
20
+ export declare const openImageEditorDialog: (options: ImageEditorDialogOptions) => Promise<DialogResult<ImageEditorDialogResult>>;
21
+ export type { CroppedFile, ImageEditorDialogOptions, ImageEditorDialogResult } from './types';
@@ -0,0 +1,25 @@
1
+ import { Dialogs } from '../../dialog';
2
+ import { default as ImageEditorDialog } from './cmp.image-editor-dialog.svelte';
3
+ /**
4
+ * Opens a full-screen image editor dialog with crop, rotate, zoom, and reset controls.
5
+ *
6
+ * ### Modes
7
+ * - **`cover`** (default) — image fills the canvas; crop selects a visible region.
8
+ * - **`contain`** — image is fitted inside the canvas with optional fill color.
9
+ *
10
+ * ### Aspect ratio
11
+ * - `number` — fixed ratio, no dropdown.
12
+ * - `{ initial, supported }` (cover) — dropdown with supported ratios.
13
+ * - `{ initial?, supported, allowFreeCrop? }` (contain) — dropdown with optional free crop.
14
+ * - Omitted — free crop in contain, full canvas in cover.
15
+ *
16
+ * ### Result
17
+ * - `croppedFile` — `FileWithBlobUrl` with `width`/`height` at natural resolution.
18
+ * - `selectedRatio` — active aspect ratio at save time (`null` for free crop).
19
+ */
20
+ export const openImageEditorDialog = async (options) => {
21
+ return await Dialogs.open({
22
+ view: ImageEditorDialog,
23
+ data: options
24
+ });
25
+ };
@@ -0,0 +1,25 @@
1
+ import type { FileWithBlobUrl } from '../../../core/files';
2
+ import type { ImgCropperContainAspectRatio, ImgCropperCoverAspectRatio } from '../img-cropper';
3
+ export type ImageEditorDialogOptions = ImageEditorDialogCoverOptions | ImageEditorDialogContainOptions;
4
+ type ImageEditorDialogOptionsBase = {
5
+ url: string;
6
+ fillColor?: string;
7
+ showImageShadow?: boolean;
8
+ };
9
+ type ImageEditorDialogCoverOptions = ImageEditorDialogOptionsBase & {
10
+ mode?: 'cover';
11
+ aspectRatio?: number | ImgCropperCoverAspectRatio;
12
+ };
13
+ type ImageEditorDialogContainOptions = ImageEditorDialogOptionsBase & {
14
+ mode: 'contain';
15
+ aspectRatio?: number | ImgCropperContainAspectRatio;
16
+ };
17
+ export type CroppedFile = FileWithBlobUrl & {
18
+ width: number;
19
+ height: number;
20
+ };
21
+ export type ImageEditorDialogResult = {
22
+ croppedFile: CroppedFile;
23
+ selectedRatio: number | null;
24
+ };
25
+ export {};
@@ -0,0 +1 @@
1
+ export {};