@milaboratories/uikit 2.2.13 → 2.2.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/uikit",
3
- "version": "2.2.13",
3
+ "version": "2.2.14",
4
4
  "type": "module",
5
5
  "main": "dist/pl-uikit.umd.js",
6
6
  "module": "dist/pl-uikit.js",
@@ -33,7 +33,7 @@
33
33
  "yarpm": "^1.2.0",
34
34
  "svgo": "^3.3.2",
35
35
  "@milaboratories/helpers": "^1.6.7",
36
- "@platforma-sdk/model": "^1.13.2"
36
+ "@platforma-sdk/model": "^1.13.5"
37
37
  },
38
38
  "scripts": {
39
39
  "dev": "vite",
@@ -0,0 +1,88 @@
1
+ <script lang="ts" setup>
2
+ import { useTransformedModel } from '@/composition/useTransformedModel';
3
+ import style from './pl-editable-title.module.scss';
4
+ import { computed, ref } from 'vue';
5
+
6
+ const model = defineModel<string>();
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ /**
11
+ * Standard input placeholder
12
+ */
13
+ placeholder?: string;
14
+ /**
15
+ * Any css `width` value (px, %), default is 80%
16
+ */
17
+ maxWidth?: string;
18
+ /**
19
+ * Fixed non-editable prefix
20
+ */
21
+ prefix?: string;
22
+ /**
23
+ * Max title length (default is 1000)
24
+ */
25
+ maxLength?: number;
26
+ /**
27
+ * Min title length
28
+ */
29
+ minLength?: number;
30
+ }>(),
31
+ {
32
+ placeholder: 'Title',
33
+ maxWidth: '80%',
34
+ prefix: undefined,
35
+ maxLength: 1000,
36
+ minLength: undefined,
37
+ },
38
+ );
39
+
40
+ const local = useTransformedModel(model, {
41
+ update() {
42
+ return false;
43
+ },
44
+ parse: (v): string => {
45
+ if (typeof v !== 'string') {
46
+ throw Error('value should be a string');
47
+ }
48
+
49
+ if (props.maxLength && v.length > props.maxLength) {
50
+ throw Error(`Max title length is ${props.maxLength} characters`);
51
+ }
52
+
53
+ if (props.minLength && v.length < props.minLength) {
54
+ throw Error(`Min title length is ${props.minLength} characters`);
55
+ }
56
+
57
+ return v.trim();
58
+ },
59
+ });
60
+
61
+ const computedStyle = computed(() => ({
62
+ maxWidth: props.maxWidth ?? '80%',
63
+ }));
64
+
65
+ const save = () => {
66
+ model.value = local.value && !local.error ? local.value : model.value;
67
+ local.reset();
68
+ };
69
+
70
+ const inputRef = ref<HTMLInputElement>();
71
+ </script>
72
+
73
+ <template>
74
+ <div class="pl-editable-title" :class="style.component" :style="computedStyle">
75
+ <div :class="style.container" @click="() => inputRef?.focus()">
76
+ <span v-if="prefix">{{ prefix.trim() }}&nbsp;</span>
77
+ <input
78
+ ref="inputRef"
79
+ v-model="local.value"
80
+ :placeholder="placeholder"
81
+ @focusout="save"
82
+ @keydown.escape="local.reset"
83
+ @keydown.enter="(ev) => (ev.target as HTMLInputElement)?.blur()"
84
+ />
85
+ </div>
86
+ <div v-if="local.error" :class="style.error">{{ local.error }}</div>
87
+ </div>
88
+ </template>
@@ -0,0 +1 @@
1
+ export { default as PlEditableTitle } from './PlEditableTitle.vue';
@@ -0,0 +1,77 @@
1
+ @import "@/assets/mixins.scss";
2
+
3
+ .component {
4
+ position: relative;
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 0;
8
+
9
+ --mask-icon-bg-color: transparent;
10
+ --mask-size: 24px;
11
+
12
+ &:hover {
13
+ --mask-icon-bg-color: var(--ic-02);
14
+ }
15
+
16
+ &:focus-within:not(&:hover) {
17
+ --mask-icon-bg-color: transparent;
18
+ }
19
+
20
+ .container {
21
+ position: relative;
22
+ display: flex;
23
+ flex-direction: row;
24
+ gap: 0;
25
+ align-items: center;
26
+ margin-right: calc(var(--mask-size));
27
+
28
+ span {
29
+ font-size: 28px;
30
+ font-weight: 500;
31
+ line-height: 32px;
32
+ letter-spacing: -0.56px;
33
+ white-space: nowrap;
34
+ }
35
+
36
+ input {
37
+ outline: none;
38
+ border: none;
39
+ text-overflow: ellipsis;
40
+ cursor: text;
41
+ field-sizing: content;
42
+ font-size: 28px;
43
+ font-weight: 500;
44
+ line-height: 32px;
45
+ letter-spacing: -0.56px;
46
+ padding-top: 4px;
47
+ padding-bottom: 4px;
48
+ padding-right: 4px;
49
+ margin: 0;
50
+ font-family: var(--font-family-base);
51
+ white-space: nowrap;
52
+ text-overflow: ellipsis;
53
+ overflow: hidden;
54
+ &::placeholder {
55
+ color: var(--txt-mask);
56
+ }
57
+ }
58
+
59
+ &::before {
60
+ content: '';
61
+ @include mask-var(url(@icons/icon-assets-min/24_edit.svg));
62
+ position: absolute;
63
+ right: calc((var(--mask-size)) * -1);
64
+ bottom: 6px;
65
+ background-color: var(--mask-icon-bg-color);
66
+ cursor: pointer;
67
+ }
68
+ }
69
+
70
+ .error {
71
+ position: absolute;
72
+ bottom: -4px;
73
+ transform: translateY(100%);
74
+ white-space: nowrap;
75
+ @include field-error();
76
+ }
77
+ }
@@ -0,0 +1,80 @@
1
+ import type { Ref, UnwrapNestedRefs } from 'vue';
2
+ import { reactive, computed, ref } from 'vue';
3
+
4
+ /**
5
+ * Creates a reactive local model with optional transformation and validation logic.
6
+ *
7
+ * @template T The type of the model's value.
8
+ *
9
+ * @param model - A `Ref` representing the underlying value.
10
+ * @param options - Optional configuration for validation and parsing.
11
+ * @param options.update - A function that takes the transformed value and returns `true` if it should be applied to the model, or `false` to keep it in a cached state.
12
+ * @param options.parse - A function that takes the input value and returns a transformed value of type `T`. If omitted, the value is used as-is.
13
+ *
14
+ * @returns A reactive object with the following properties:
15
+ * - `value`: A computed property for getting and setting the model value.
16
+ * - `error`: A `Ref<string | undefined>` containing the last error message, if any.
17
+ * - `reset`: A method to clear the cached value and error state.
18
+ *
19
+ * ### Example
20
+ * ```ts
21
+ * import { ref } from 'vue';
22
+ * import { useTransformedModel } from './useTransformedModel';
23
+ *
24
+ * const model = ref<number>(42);
25
+ *
26
+ * const transformedModel = useTransformedModel(model, {
27
+ * parse: (value) => {
28
+ * const parsed = Number(value);
29
+ * if (!Number.isFinite(parsed)) throw new Error('Invalid number');
30
+ * return parsed;
31
+ * },
32
+ * update: (value) => value >= 0, // Only allow non-negative numbers
33
+ * });
34
+ */
35
+
36
+ export function useTransformedModel<T>(model: Ref<T>, options: { update?: (v: T) => boolean; parse?: (v: unknown) => T }) {
37
+ const cached = ref<T | undefined>();
38
+ const error = ref<string>();
39
+
40
+ const { parse, update } = options;
41
+
42
+ const reset = () => {
43
+ cached.value = undefined;
44
+ error.value = undefined;
45
+ };
46
+
47
+ const value = computed<T>({
48
+ get() {
49
+ if (cached.value !== undefined) {
50
+ return cached.value;
51
+ }
52
+
53
+ return model.value;
54
+ },
55
+ set(value) {
56
+ reset();
57
+
58
+ try {
59
+ const newValue = parse ? parse(value) : value;
60
+
61
+ const shouldUpdate = update ? update(newValue) : true;
62
+
63
+ if (shouldUpdate) {
64
+ model.value = newValue;
65
+ } else {
66
+ cached.value = newValue;
67
+ }
68
+ } catch (err) {
69
+ cached.value = value;
70
+ error.value = err instanceof Error ? err.message : String(err);
71
+ }
72
+ },
73
+ });
74
+
75
+ return reactive({
76
+ value,
77
+ error,
78
+ reset,
79
+ }) as UnwrapNestedRefs<{ value: T; error?: string; reset: () => void }>;
80
+ }
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from './components/PlBtnSecondary';
26
26
  export * from './components/PlBtnGhost';
27
27
  export * from './components/PlBtnLink';
28
28
  export * from './components/PlBtnGroup';
29
+ export * from './components/PlEditableTitle';
29
30
  export * from './components/PlTextField';
30
31
  export * from './components/PlTextArea';
31
32
  export * from './components/PlDropdown';
@@ -19,7 +19,7 @@ defineProps<{
19
19
  <template>
20
20
  <div class="pl-layout-component pl-block-page" :class="{ noBodyGutters }">
21
21
  <div v-if="slots.title" class="pl-block-page__title">
22
- <h1><slot name="title" /></h1>
22
+ <div class="pl-block-page__title__default"><slot name="title" /></div>
23
23
  <div class="pl-block-page__title__append">
24
24
  <slot name="append" />
25
25
  </div>
@@ -21,13 +21,17 @@
21
21
  gap: 12px;
22
22
  padding: 20px 24px;
23
23
 
24
- h1 {
24
+ &__default {
25
25
  margin: 0;
26
26
  color: var(--txt-01);
27
27
  font-size: 28px;
28
28
  font-weight: 500;
29
29
  line-height: 32px;
30
30
  letter-spacing: -0.56px;
31
+ display: flex;
32
+ flex-direction: row;
33
+ align-items: center;
34
+ gap: 12px;
31
35
  }
32
36
 
33
37
  &__append {
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ImportFileHandle, Platforma, StorageHandle, Ref as ModelRef } from '@platforma-sdk/model';
1
+ import type { ImportFileHandle, Platforma, StorageHandle, PlRef as ModelRef } from '@platforma-sdk/model';
2
2
  import type { Ref, ComputedRef } from 'vue';
3
3
  import { maskIcons16 } from './generated/icons-16';
4
4
  import { maskIcons24 } from './generated/icons-24';