@streamscloud/kit 0.1.9 → 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.
- package/dist/core/utils/color-helper.d.ts +13 -0
- package/dist/core/utils/color-helper.js +39 -0
- package/dist/core/utils/index.d.ts +1 -0
- package/dist/core/utils/index.js +1 -0
- package/dist/ui/color-picker/cmp.color-picker.svelte +150 -0
- package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +33 -0
- package/dist/ui/color-picker/cmp.input-stub.svelte +98 -0
- package/dist/ui/color-picker/cmp.input-stub.svelte.d.ts +40 -0
- package/dist/ui/color-picker/color-picker-localization.d.ts +3 -0
- package/dist/ui/color-picker/color-picker-localization.js +12 -0
- package/dist/ui/color-picker/index.d.ts +1 -0
- package/dist/ui/color-picker/index.js +1 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte +109 -0
- package/dist/ui/cropper/image-editor-dialog/cmp.image-editor-dialog.svelte.d.ts +9 -0
- package/dist/ui/cropper/image-editor-dialog/image-editor-dialog-localization.d.ts +6 -0
- package/dist/ui/cropper/image-editor-dialog/image-editor-dialog-localization.js +33 -0
- package/dist/ui/cropper/image-editor-dialog/index.d.ts +21 -0
- package/dist/ui/cropper/image-editor-dialog/index.js +25 -0
- package/dist/ui/cropper/image-editor-dialog/types.d.ts +25 -0
- package/dist/ui/cropper/image-editor-dialog/types.js +1 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte +67 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-controls.svelte.d.ts +21 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte +228 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper-toolbar.svelte.d.ts +28 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte +198 -0
- package/dist/ui/cropper/img-cropper/cmp.img-cropper.svelte.d.ts +58 -0
- package/dist/ui/cropper/img-cropper/cropperjs-elements.d.ts +33 -0
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.d.ts +40 -0
- package/dist/ui/cropper/img-cropper/img-cropper-contain-worker.svelte.js +159 -0
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.d.ts +40 -0
- package/dist/ui/cropper/img-cropper/img-cropper-cover-worker.svelte.js +163 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.d.ts +6 -0
- package/dist/ui/cropper/img-cropper/img-cropper-localization.js +33 -0
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.d.ts +11 -0
- package/dist/ui/cropper/img-cropper/img-cropper-toolbar-localization.js +68 -0
- package/dist/ui/cropper/img-cropper/img-cropper-utils.d.ts +32 -0
- package/dist/ui/cropper/img-cropper/img-cropper-utils.js +138 -0
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.d.ts +39 -0
- package/dist/ui/cropper/img-cropper/img-cropper-worker.svelte.js +1 -0
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.d.ts +81 -0
- package/dist/ui/cropper/img-cropper/img-cropper.svelte.js +160 -0
- package/dist/ui/cropper/img-cropper/index.d.ts +4 -0
- package/dist/ui/cropper/img-cropper/index.js +4 -0
- package/dist/ui/icon-text/cmp.icon-text.svelte +90 -0
- package/dist/ui/icon-text/cmp.icon-text.svelte.d.ts +39 -0
- package/dist/ui/icon-text/index.d.ts +1 -0
- package/dist/ui/icon-text/index.js +1 -0
- package/package.json +27 -5
|
@@ -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
|
+
}
|
package/dist/core/utils/index.js
CHANGED
|
@@ -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 @@
|
|
|
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,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 {};
|