@karyl-chan/ui 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karyl-chan/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Shared Vue UI primitives, composables, and design tokens used by karyl-chan's bot frontend and plugin webuis.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,13 +22,12 @@
|
|
|
22
22
|
"./use-drawer.css": "./src/composables/use-drawer.css",
|
|
23
23
|
"./use-popover.css": "./src/composables/use-popover.css"
|
|
24
24
|
},
|
|
25
|
-
"files": [
|
|
25
|
+
"files": [
|
|
26
|
+
"src"
|
|
27
|
+
],
|
|
26
28
|
"engines": {
|
|
27
29
|
"node": ">=22"
|
|
28
30
|
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"typecheck": "vue-tsc --noEmit"
|
|
31
|
-
},
|
|
32
31
|
"peerDependencies": {
|
|
33
32
|
"@iconify/vue": ">=5",
|
|
34
33
|
"@popperjs/core": ">=2",
|
|
@@ -38,7 +37,9 @@
|
|
|
38
37
|
"vue-router": ">=4"
|
|
39
38
|
},
|
|
40
39
|
"peerDependenciesMeta": {
|
|
41
|
-
"vue-router": {
|
|
40
|
+
"vue-router": {
|
|
41
|
+
"optional": true
|
|
42
|
+
}
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@iconify/vue": "^5.0.0",
|
|
@@ -54,5 +55,8 @@
|
|
|
54
55
|
"vue": "^3.5.13",
|
|
55
56
|
"vue-router": "^4.5.0",
|
|
56
57
|
"vue-tsc": "^2.2.0"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"typecheck": "vue-tsc --noEmit"
|
|
57
61
|
}
|
|
58
|
-
}
|
|
62
|
+
}
|
|
@@ -70,8 +70,8 @@ withDefaults(defineProps<{
|
|
|
70
70
|
/>
|
|
71
71
|
</slot>
|
|
72
72
|
|
|
73
|
-
<!-- 主要 label -->
|
|
74
|
-
<span class="app-btn-label"><slot /></span>
|
|
73
|
+
<!-- 主要 label — 略過時不渲染,避免 icon-only 按鈕產生多餘 gap -->
|
|
74
|
+
<span v-if="$slots.default" class="app-btn-label"><slot /></span>
|
|
75
75
|
|
|
76
76
|
<!-- 右側 slot -->
|
|
77
77
|
<slot name="trailing" />
|
|
@@ -85,11 +85,9 @@ const emit = defineEmits<{
|
|
|
85
85
|
</template>
|
|
86
86
|
|
|
87
87
|
<style scoped>
|
|
88
|
-
/* Inner wrapper:
|
|
89
|
-
AppModal's body
|
|
90
|
-
component is responsible for its own spacing. */
|
|
88
|
+
/* Inner wrapper: flex-column layout + gap. Padding comes from
|
|
89
|
+
AppModal's body default. */
|
|
91
90
|
.acd-body {
|
|
92
|
-
padding: 0.9rem 1rem 0.75rem;
|
|
93
91
|
display: flex;
|
|
94
92
|
flex-direction: column;
|
|
95
93
|
gap: 0.75rem;
|
|
@@ -13,7 +13,11 @@ import { useDrawer } from '../composables/use-drawer';
|
|
|
13
13
|
* The caller owns everything inside the slot — typical usage is to drop
|
|
14
14
|
* a `<form @submit.prevent>` containing fields + an actions footer
|
|
15
15
|
* directly into the default slot. AppModal supplies the chrome (backdrop,
|
|
16
|
-
* panel, optional title header, close button)
|
|
16
|
+
* panel, optional title header, close button) and pads the body with
|
|
17
|
+
* `padding` (default `0.8rem 0.9rem`, both viewport branches). Slot
|
|
18
|
+
* content therefore needs no padding of its own; pass `padding="0"` for
|
|
19
|
+
* full-bleed content (tables, pickers, croppers) that manages its own
|
|
20
|
+
* edges.
|
|
17
21
|
*
|
|
18
22
|
* `closeOnBackdrop` / `closeOnEscape` default to true. Escape closure on
|
|
19
23
|
* desktop is wired locally; on mobile it's handled by useDrawer's escape
|
|
@@ -27,11 +31,17 @@ const props = withDefaults(defineProps<{
|
|
|
27
31
|
closeOnEscape?: boolean;
|
|
28
32
|
/** Width of the desktop panel; mobile drawer ignores this. */
|
|
29
33
|
width?: string;
|
|
34
|
+
/**
|
|
35
|
+
* CSS padding of the scrollable body around the default slot. Use
|
|
36
|
+
* `'0'` for full-bleed content that manages its own edges.
|
|
37
|
+
*/
|
|
38
|
+
padding?: string;
|
|
30
39
|
}>(), {
|
|
31
40
|
title: '',
|
|
32
41
|
closeOnBackdrop: true,
|
|
33
42
|
closeOnEscape: true,
|
|
34
|
-
width: 'min(440px, 92vw)'
|
|
43
|
+
width: 'min(440px, 92vw)',
|
|
44
|
+
padding: '0.8rem 0.9rem'
|
|
35
45
|
});
|
|
36
46
|
|
|
37
47
|
const emit = defineEmits<{
|
|
@@ -101,7 +111,7 @@ function onBackdropClick() {
|
|
|
101
111
|
<Icon icon="material-symbols:close-rounded" width="18" height="18" />
|
|
102
112
|
</button>
|
|
103
113
|
</header>
|
|
104
|
-
<div class="app-modal-body">
|
|
114
|
+
<div class="app-modal-body" :style="{ padding }">
|
|
105
115
|
<slot />
|
|
106
116
|
</div>
|
|
107
117
|
</div>
|
|
@@ -141,7 +151,7 @@ function onBackdropClick() {
|
|
|
141
151
|
<Icon icon="material-symbols:close-rounded" width="18" height="18" />
|
|
142
152
|
</button>
|
|
143
153
|
</header>
|
|
144
|
-
<div class="app-modal-drawer-body">
|
|
154
|
+
<div class="app-modal-drawer-body" :style="{ padding }">
|
|
145
155
|
<slot />
|
|
146
156
|
</div>
|
|
147
157
|
</div>
|
|
@@ -245,9 +255,8 @@ function onBackdropClick() {
|
|
|
245
255
|
|
|
246
256
|
/* ── Desktop body wrapper ───────────────────────────────────────── */
|
|
247
257
|
/* Structural wrapper that matches the mobile .app-modal-drawer-body.
|
|
248
|
-
Padding
|
|
249
|
-
|
|
250
|
-
supply their own inner padding. */
|
|
258
|
+
Padding comes from the `padding` prop (inline style) on both
|
|
259
|
+
branches so slot content needs none of its own. */
|
|
251
260
|
.app-modal-body {
|
|
252
261
|
flex: 1;
|
|
253
262
|
min-height: 0;
|
|
@@ -19,6 +19,8 @@ const props = withDefaults(defineProps<{
|
|
|
19
19
|
maxlength?: number;
|
|
20
20
|
fullWidth?: boolean;
|
|
21
21
|
name?: string;
|
|
22
|
+
autofocus?: boolean;
|
|
23
|
+
required?: boolean;
|
|
22
24
|
}>(), {
|
|
23
25
|
label: '',
|
|
24
26
|
hint: '',
|
|
@@ -28,7 +30,9 @@ const props = withDefaults(defineProps<{
|
|
|
28
30
|
readonly: false,
|
|
29
31
|
rows: 3,
|
|
30
32
|
fullWidth: false,
|
|
31
|
-
name: ''
|
|
33
|
+
name: '',
|
|
34
|
+
autofocus: false,
|
|
35
|
+
required: false
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
const emit = defineEmits<{
|
|
@@ -64,6 +68,8 @@ const rootClass = computed(() => ({
|
|
|
64
68
|
:rows="rows"
|
|
65
69
|
:maxlength="maxlength"
|
|
66
70
|
:name="name || undefined"
|
|
71
|
+
:autofocus="autofocus"
|
|
72
|
+
:required="required"
|
|
67
73
|
@input="onInput"
|
|
68
74
|
@blur="emit('blur', $event)"
|
|
69
75
|
@focus="emit('focus', $event)"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts" generic="V extends string | number | null">
|
|
1
|
+
<script setup lang="ts" generic="V extends string | number | null | undefined">
|
|
2
2
|
import { computed, useId } from 'vue';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -17,6 +17,8 @@ import { computed, useId } from 'vue';
|
|
|
17
17
|
|
|
18
18
|
const props = withDefaults(defineProps<{
|
|
19
19
|
modelValue: V;
|
|
20
|
+
/** v-model modifier bag (e.g. `.number`, `.trim`); Vue populates this. */
|
|
21
|
+
modelModifiers?: { number?: boolean; trim?: boolean; lazy?: boolean };
|
|
20
22
|
label?: string;
|
|
21
23
|
hint?: string;
|
|
22
24
|
error?: string;
|
|
@@ -70,7 +72,16 @@ const fieldId = useId();
|
|
|
70
72
|
|
|
71
73
|
function onInput(e: Event): void {
|
|
72
74
|
const target = e.target as HTMLInputElement;
|
|
73
|
-
|
|
75
|
+
let value: string | number = target.value;
|
|
76
|
+
if (props.modelModifiers?.trim) value = (value as string).trim();
|
|
77
|
+
if (props.modelModifiers?.number) {
|
|
78
|
+
const n = Number(value);
|
|
79
|
+
// Mirror Vue's `.number` semantics: only coerce when parseable;
|
|
80
|
+
// otherwise pass through the raw string so the input stays
|
|
81
|
+
// editable during typing (e.g. "1.").
|
|
82
|
+
if (!Number.isNaN(n) && value !== '') value = n;
|
|
83
|
+
}
|
|
84
|
+
emit('update:modelValue', value as V);
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
const rootClass = computed(() => ({
|