@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.1.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": ["src"],
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": { "optional": true }
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: provides padding + flex-column layout + gap.
89
- AppModal's body slots are structural-only (no padding), so each leaf
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) only.
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 is intentionally 0 — callers are responsible for spacing
249
- their slot content. AppConfirmDialog and similar leaf components
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
- emit('update:modelValue', target.value as V);
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(() => ({