vanilla-jui 1.4.8 → 1.5.2

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 (58) hide show
  1. package/README.md +10 -4
  2. package/dist/index.d.ts +402 -977
  3. package/dist/index.js +1 -1
  4. package/dist/index.umd.js +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +3 -3
  7. package/src/components/accordion.ts +467 -349
  8. package/src/components/flow.ts +779 -1133
  9. package/src/components/form.ts +666 -509
  10. package/src/components/menu.ts +199 -326
  11. package/src/components/modal.ts +510 -747
  12. package/src/components/offcanvas.ts +281 -361
  13. package/src/components/pagination.ts +207 -374
  14. package/src/components/swiper.ts +688 -1168
  15. package/src/components/tabs.ts +468 -788
  16. package/src/components/toc.ts +191 -244
  17. package/src/core/component.ts +332 -0
  18. package/src/core/motion.ts +298 -0
  19. package/src/core/presence.ts +233 -0
  20. package/src/core/scheduler.ts +91 -0
  21. package/src/core/view.ts +31 -0
  22. package/src/css/accordion.css +94 -3
  23. package/src/css/color.css +4 -5
  24. package/src/css/color_p3.css +6 -5
  25. package/src/css/content.css +18 -24
  26. package/src/css/drop.css +4 -1
  27. package/src/css/form.css +8 -0
  28. package/src/css/gradient.css +64 -131
  29. package/src/css/layout.css +9 -23
  30. package/src/css/menu.css +10 -5
  31. package/src/css/modal.css +2 -1
  32. package/src/css/offcanvas.css +1 -17
  33. package/src/css/swiper.css +2 -1
  34. package/src/css/tabs.css +8 -17
  35. package/src/css/themes/default/accordion.css +0 -48
  36. package/src/css/themes/default/swiper.css +1 -1
  37. package/src/css/themes/default/tabs.css +4 -6
  38. package/src/css/toast.css +1 -17
  39. package/src/css/tokens.css +18 -35
  40. package/src/css/tooltip.css +2 -1
  41. package/src/index.ts +8 -3
  42. package/src/locales/index.ts +3 -0
  43. package/src/primitives/drop.ts +253 -262
  44. package/src/primitives/loading.ts +22 -4
  45. package/src/primitives/parabola.ts +123 -164
  46. package/src/primitives/sticky.ts +297 -0
  47. package/src/primitives/theme.ts +164 -222
  48. package/src/primitives/toast.ts +309 -284
  49. package/src/primitives/tooltip.ts +65 -66
  50. package/src/utilities/browser.ts +38 -0
  51. package/src/utilities/dom.ts +0 -54
  52. package/src/utilities/refs.ts +61 -0
  53. package/src/utilities/state.ts +73 -0
  54. package/src/utilities/timer.ts +11 -31
  55. package/src/validation/validator.ts +289 -389
  56. package/src/components/sticky.ts +0 -297
  57. package/src/core/Component.ts +0 -377
  58. package/src/utilities/scheduler.ts +0 -156
@@ -0,0 +1,233 @@
1
+ import { flushSync } from 'vanilla-signal';
2
+
3
+ import { type MotionController } from './motion.ts';
4
+
5
+ export type PresencePhase = 'hidden' | 'entering' | 'visible' | 'leaving';
6
+
7
+ export interface PresenceOptions {
8
+ elements: () => readonly (Element | null | undefined)[];
9
+ mount: () => void;
10
+ activate: () => void;
11
+ deactivate: () => void;
12
+ unmount: () => void;
13
+ motion?: MotionController;
14
+ }
15
+
16
+ export interface PresenceController {
17
+ readonly phase: PresencePhase;
18
+ enter: () => Promise<boolean>;
19
+ leave: () => Promise<boolean>;
20
+ cancel: () => void;
21
+ }
22
+
23
+ function connectedElements(
24
+ values: readonly (Element | null | undefined)[]
25
+ ): Element[] {
26
+ return values.filter(
27
+ (value): value is Element => value != null && value.isConnected
28
+ );
29
+ }
30
+
31
+ function commitInitialStyles(elements: readonly Element[]): void {
32
+ elements[0]?.getBoundingClientRect();
33
+ }
34
+
35
+ function timeList(value: string): number[] {
36
+ return value.split(',').map((part) => {
37
+ const time = part.trim();
38
+ if (time.endsWith('ms')) return Number.parseFloat(time) || 0;
39
+ if (time.endsWith('s')) return (Number.parseFloat(time) || 0) * 1000;
40
+ return 0;
41
+ });
42
+ }
43
+
44
+ function numberList(value: string): number[] {
45
+ return value.split(',').map((part) => {
46
+ const count = Number.parseFloat(part);
47
+ return Number.isFinite(count) ? count : 0;
48
+ });
49
+ }
50
+
51
+ function timelineDuration(
52
+ durations: number[],
53
+ delays: number[],
54
+ iterations: number[] = [1],
55
+ enabled: boolean[] = [true]
56
+ ): number {
57
+ const count = Math.max(
58
+ durations.length,
59
+ delays.length,
60
+ iterations.length,
61
+ enabled.length
62
+ );
63
+ let duration = 0;
64
+ for (let index = 0; index < count; index += 1) {
65
+ if (!enabled[index % enabled.length]) continue;
66
+ const active =
67
+ (durations[index % durations.length] || 0) *
68
+ (iterations[index % iterations.length] || 0) +
69
+ (delays[index % delays.length] || 0);
70
+ duration = Math.max(duration, active);
71
+ }
72
+ return Math.max(0, duration);
73
+ }
74
+
75
+ function computedMotionDuration(element: Element): number {
76
+ const view = element.ownerDocument.defaultView;
77
+ if (!view) return 0;
78
+ const style = view.getComputedStyle(element);
79
+ const transition = timelineDuration(
80
+ timeList(style.transitionDuration),
81
+ timeList(style.transitionDelay),
82
+ [1],
83
+ style.transitionProperty
84
+ .split(',')
85
+ .map((property) => property.trim() !== 'none')
86
+ );
87
+ const animation = timelineDuration(
88
+ timeList(style.animationDuration),
89
+ timeList(style.animationDelay),
90
+ numberList(style.animationIterationCount),
91
+ style.animationName.split(',').map((name) => name.trim() !== 'none')
92
+ );
93
+ return Math.max(transition, animation);
94
+ }
95
+
96
+ function waitForDuration(
97
+ duration: number,
98
+ signal?: AbortSignal
99
+ ): Promise<void> {
100
+ return new Promise((resolve) => {
101
+ if (signal?.aborted) {
102
+ resolve();
103
+ return;
104
+ }
105
+ const finish = (): void => {
106
+ clearTimeout(timeout);
107
+ signal?.removeEventListener('abort', finish);
108
+ resolve();
109
+ };
110
+ const timeout = setTimeout(finish, duration);
111
+ signal?.addEventListener('abort', finish, { once: true });
112
+ });
113
+ }
114
+
115
+ export async function waitForMotion(
116
+ elements: readonly Element[],
117
+ signal?: AbortSignal
118
+ ): Promise<void> {
119
+ const animations = new Set<Animation>();
120
+ let fallbackDuration = 0;
121
+ for (const element of elements) {
122
+ if (typeof element.getAnimations !== 'function') {
123
+ fallbackDuration = Math.max(
124
+ fallbackDuration,
125
+ computedMotionDuration(element)
126
+ );
127
+ continue;
128
+ }
129
+ for (const animation of element.getAnimations()) {
130
+ const endTime = animation.effect?.getComputedTiming().endTime;
131
+ if (typeof endTime !== 'number' || Number.isFinite(endTime)) {
132
+ animations.add(animation);
133
+ }
134
+ }
135
+ }
136
+ const pending = Array.from(
137
+ animations,
138
+ (animation) => animation.finished as Promise<unknown>
139
+ );
140
+ if (fallbackDuration > 0) {
141
+ pending.push(waitForDuration(fallbackDuration, signal));
142
+ }
143
+ if (pending.length === 0) return;
144
+ const settled = Promise.allSettled(pending);
145
+ if (!signal) {
146
+ await settled;
147
+ return;
148
+ }
149
+ await new Promise<void>((resolve) => {
150
+ const finish = (): void => {
151
+ signal.removeEventListener('abort', finish);
152
+ resolve();
153
+ };
154
+ if (signal.aborted) {
155
+ finish();
156
+ return;
157
+ }
158
+ signal.addEventListener('abort', finish, { once: true });
159
+ void settled.then(finish);
160
+ });
161
+ }
162
+
163
+ export function createPresence(options: PresenceOptions): PresenceController {
164
+ let phase: PresencePhase = 'hidden';
165
+ let operation = 0;
166
+ let entering: Promise<boolean> | null = null;
167
+ let leaving: Promise<boolean> | null = null;
168
+ let motionController: AbortController | null = null;
169
+
170
+ const nextMotion = (): AbortSignal => {
171
+ motionController?.abort();
172
+ motionController = new AbortController();
173
+ return motionController.signal;
174
+ };
175
+
176
+ const enter = (): Promise<boolean> => {
177
+ if (phase === 'visible') return Promise.resolve(false);
178
+ if (phase === 'entering' && entering) return entering;
179
+
180
+ const currentOperation = ++operation;
181
+ const signal = nextMotion();
182
+ phase = 'entering';
183
+ entering = (async () => {
184
+ options.mount();
185
+ const elements = connectedElements(options.elements());
186
+ commitInitialStyles(elements);
187
+ flushSync(options.activate);
188
+ if (options.motion) await options.motion.enter(signal);
189
+ else await waitForMotion(elements, signal);
190
+ if (currentOperation !== operation) return false;
191
+ phase = 'visible';
192
+ return true;
193
+ })();
194
+ return entering;
195
+ };
196
+
197
+ const leave = (): Promise<boolean> => {
198
+ if (phase === 'hidden') return Promise.resolve(false);
199
+ if (phase === 'leaving' && leaving) return leaving;
200
+
201
+ const currentOperation = ++operation;
202
+ const signal = nextMotion();
203
+ phase = 'leaving';
204
+ leaving = (async () => {
205
+ flushSync(options.deactivate);
206
+ const elements = connectedElements(options.elements());
207
+ if (options.motion) await options.motion.leave(signal);
208
+ else await waitForMotion(elements, signal);
209
+ if (currentOperation !== operation) return false;
210
+ options.unmount();
211
+ phase = 'hidden';
212
+ return true;
213
+ })();
214
+ return leaving;
215
+ };
216
+
217
+ return {
218
+ get phase() {
219
+ return phase;
220
+ },
221
+ enter,
222
+ leave,
223
+ cancel() {
224
+ operation += 1;
225
+ motionController?.abort();
226
+ motionController = null;
227
+ options.motion?.cancel();
228
+ phase = 'hidden';
229
+ entering = null;
230
+ leaving = null;
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,91 @@
1
+ interface QueueNode {
2
+ queued: boolean;
3
+ enqueued: boolean;
4
+ disposed: boolean;
5
+ next: QueueNode | null;
6
+ run: () => void;
7
+ }
8
+
9
+ interface ScheduledTask {
10
+ schedule: () => void;
11
+ flush: () => void;
12
+ cancel: () => void;
13
+ dispose: () => void;
14
+ }
15
+
16
+ let head: QueueNode | null = null;
17
+ let tail: QueueNode | null = null;
18
+ let pending = false;
19
+
20
+ function queueTask(callback: () => void): void {
21
+ if (typeof queueMicrotask === 'function') {
22
+ queueMicrotask(callback);
23
+ return;
24
+ }
25
+ void Promise.resolve().then(callback);
26
+ }
27
+
28
+ function requestFlush(): void {
29
+ if (pending) return;
30
+ pending = true;
31
+ queueTask(flushQueue);
32
+ }
33
+
34
+ function enqueue(node: QueueNode): void {
35
+ node.enqueued = true;
36
+ if (tail) tail.next = node;
37
+ else head = node;
38
+ tail = node;
39
+ requestFlush();
40
+ }
41
+
42
+ function flushQueue(): void {
43
+ pending = false;
44
+ const start = head;
45
+ head = null;
46
+ tail = null;
47
+
48
+ let node = start;
49
+ while (node) {
50
+ const next = node.next;
51
+ node.next = null;
52
+ node.enqueued = false;
53
+
54
+ if (!node.disposed && node.queued) {
55
+ node.queued = false;
56
+ node.run();
57
+ }
58
+
59
+ node = next;
60
+ }
61
+ }
62
+
63
+ export function createScheduledTask(run: () => void): ScheduledTask {
64
+ const node: QueueNode = {
65
+ queued: false,
66
+ enqueued: false,
67
+ disposed: false,
68
+ next: null,
69
+ run,
70
+ };
71
+
72
+ return {
73
+ schedule() {
74
+ if (node.disposed || node.queued) return;
75
+ node.queued = true;
76
+ if (!node.enqueued) enqueue(node);
77
+ },
78
+ flush() {
79
+ if (node.disposed) return;
80
+ node.queued = false;
81
+ node.run();
82
+ },
83
+ cancel() {
84
+ node.queued = false;
85
+ },
86
+ dispose() {
87
+ node.disposed = true;
88
+ node.queued = false;
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,31 @@
1
+ import { createRoot, onCleanup, untrack } from 'vanilla-signal';
2
+
3
+ export interface OwnedView<TElement extends Element> {
4
+ element: TElement;
5
+ dispose: () => void;
6
+ }
7
+
8
+ export interface OwnedViewOptions {
9
+ removeOnDispose?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Creates one stable view inside a vanilla-signal owner.
14
+ * Reactive accessors declared by the factory retain the owner, while the
15
+ * factory itself is not turned into a replaceable dynamic region.
16
+ */
17
+ export function createOwnedView<TElement extends Element>(
18
+ factory: () => TElement,
19
+ options: OwnedViewOptions = {}
20
+ ): OwnedView<TElement> {
21
+ let element!: TElement;
22
+ const removeOnDispose = options.removeOnDispose !== false;
23
+
24
+ const dispose = createRoot((disposeRoot) => {
25
+ element = untrack(factory);
26
+ if (removeOnDispose) onCleanup(() => element.remove());
27
+ return disposeRoot;
28
+ }) as () => void;
29
+
30
+ return { element, dispose };
31
+ }
@@ -1,9 +1,51 @@
1
1
  @layer components {
2
+ .j-accordion {
3
+ display: flex;
4
+ flex-direction: column;
5
+ }
6
+
7
+ .j-accordion[data-direction='horizontal'] {
8
+ flex-direction: row;
9
+ }
10
+
2
11
  .j-accordion .accordion-header {
3
12
  display: flex;
4
13
  align-items: center;
5
14
  cursor: pointer;
6
15
  user-select: none;
16
+
17
+ padding: var(--accordion-header-padding);
18
+ margin: var(--accordion-header-margin);
19
+ font-weight: var(--accordion-font-weight);
20
+ font-size: var(--accordion-header-font-size);
21
+ background: var(--accordion-bg);
22
+ border-bottom: var(--accordion-border);
23
+ }
24
+
25
+ .j-accordion[data-direction='horizontal'] .accordion-header {
26
+ gap: var(--space);
27
+ border-bottom: 0;
28
+ border-right: var(--accordion-border);
29
+ }
30
+
31
+ .j-accordion .accordion-header[aria-expanded='true'] {
32
+ border-bottom: 0;
33
+ }
34
+
35
+ .j-accordion[data-direction='horizontal'] .accordion-header[aria-expanded='true'] {
36
+ border-right: 0;
37
+ }
38
+
39
+ .j-accordion .accordion-header[aria-expanded='false']:hover {
40
+ --accordion-bg: var(--ui-surface-hover);
41
+ }
42
+
43
+ .j-accordion .accordion-header[aria-expanded='false']:nth-last-child(2) {
44
+ border-bottom: 0;
45
+ }
46
+
47
+ .j-accordion[data-direction='horizontal'] .accordion-header[aria-expanded='false']:nth-last-child(2) {
48
+ border-right: 0;
7
49
  }
8
50
 
9
51
  .j-accordion .accordion-header .header-title {
@@ -11,17 +53,66 @@
11
53
  text-align: left;
12
54
  }
13
55
 
56
+ .j-accordion .accordion-header[aria-expanded='true'] .header-title {
57
+ --accordion-font-weight: var(--font-bold);
58
+ }
59
+
14
60
  .j-accordion .accordion-header .header-arrow {
15
61
  display: flex;
16
62
  justify-content: center;
17
63
  align-items: center;
64
+ transition: transform 0.3s ease-out;
65
+ transform: var(--accordion-icon-transform);
66
+ }
67
+
68
+ .j-accordion[data-direction='horizontal'] .accordion-header .header-arrow {
69
+ --accordion-icon-transform: rotate(-90deg);
70
+ }
71
+
72
+ .j-accordion .accordion-header[aria-expanded='true'] .header-arrow {
73
+ --accordion-icon-transform: rotate(180deg);
74
+ }
75
+
76
+ .j-accordion[data-direction='horizontal'] .accordion-header[aria-expanded='true'] .header-arrow {
77
+ --accordion-icon-transform: rotate(90deg);
18
78
  }
19
79
 
20
80
  .j-accordion .accordion-panel {
21
- display: none;
81
+ margin: var(--accordion-panel-margin);
82
+ font-size: var(--accordion-font-size);
83
+ border-bottom: var(--accordion-border);
84
+ position: relative;
22
85
  }
23
86
 
24
87
  .j-accordion .accordion-panel[aria-hidden='false'] {
25
- display: block;
88
+ min-height: 2rem;
89
+ }
90
+
91
+ .j-accordion[data-direction='horizontal'] .accordion-panel[aria-hidden='false'] {
92
+ border-right: var(--accordion-border);
93
+ border-bottom: 0;
94
+ flex: 1;
95
+ }
96
+
97
+ .j-accordion .accordion-panel:last-child {
98
+ border-bottom: 0;
99
+ }
100
+
101
+ .j-accordion[data-direction='horizontal'] .accordion-panel:last-child {
102
+ border-right: 0;
103
+ }
104
+
105
+ .j-accordion .accordion-panel .panel-content {
106
+ margin: var(--accordion-panel-content-margin);
107
+ padding: var(--accordion-panel-content-padding);
108
+ position: relative;
109
+ min-height: inherit;
110
+ }
111
+
112
+ .j-accordion[data-direction='horizontal'] .accordion-panel[aria-hidden='false'] .panel-content {
113
+ display: flex;
114
+ flex-direction: column;
115
+ justify-content: center;
116
+ height: 100%;
26
117
  }
27
- }
118
+ }
package/src/css/color.css CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  @layer color {
6
+
6
7
  /**
7
8
  * gray, olive,
8
9
  * tomato, ruby, pink,
@@ -11,8 +12,7 @@
11
12
  * yellow, orange, gold
12
13
  */
13
14
  :root,
14
- .light,
15
- .light-theme {
15
+ .light {
16
16
  --white: #ffffff;
17
17
  --black: #000000;
18
18
 
@@ -497,8 +497,7 @@
497
497
  --gold-track: #b45309;
498
498
  }
499
499
 
500
- .dark,
501
- .dark-theme {
500
+ .dark {
502
501
  --white: #000000;
503
502
  --black: #ffffff;
504
503
 
@@ -982,4 +981,4 @@
982
981
  --gold-indicator: #fbbf24;
983
982
  --gold-track: #fbbf24;
984
983
  }
985
- }
984
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  @layer color {
6
+
6
7
  /**
7
8
  * Advanced color mode: display-p3
8
9
  *
@@ -14,9 +15,9 @@
14
15
  */
15
16
  @supports (color: color(display-p3 1 1 1)) {
16
17
  @media (color-gamut: p3) {
18
+
17
19
  :root,
18
- .light,
19
- .light-theme {
20
+ .light {
20
21
  /* gray */
21
22
  --gray-1: oklch(99.3% 0.0028 259.7);
22
23
  --gray-2: oklch(98% 0.0053 259.7);
@@ -502,8 +503,8 @@
502
503
 
503
504
  @supports (color: color(display-p3 1 1 1)) {
504
505
  @media (color-gamut: p3) {
505
- .dark,
506
- .dark-theme {
506
+
507
+ .dark {
507
508
  /* gray */
508
509
  --gray-1: oklch(17.8% 0.0034 264.5);
509
510
  --gray-2: oklch(21% 0.0035 264.5);
@@ -986,4 +987,4 @@
986
987
  }
987
988
  }
988
989
  }
989
- }
990
+ }
@@ -1,4 +1,5 @@
1
1
  @layer content {
2
+
2
3
  /* ============================================
3
4
  Content - 文章阅读/编辑器预览样式
4
5
  ============================================ */
@@ -18,12 +19,6 @@
18
19
  /* ============================================
19
20
  Typography - 段落与标题
20
21
  ============================================ */
21
- .j-content *,
22
- .j-content *::before,
23
- .j-content *::after {
24
- box-sizing: border-box;
25
- }
26
-
27
22
  .j-content p {
28
23
  margin: 0 0 var(--content-paragraph-space);
29
24
  }
@@ -34,8 +29,7 @@
34
29
  .j-content h4,
35
30
  .j-content h5,
36
31
  .j-content h6 {
37
- margin: var(--content-heading-space) 0
38
- calc(var(--content-heading-space) * 0.75);
32
+ margin: var(--content-heading-space) 0 calc(var(--content-heading-space) * 0.75);
39
33
  font-weight: 600;
40
34
  line-height: 1.3;
41
35
  color: var(--content-heading-fg);
@@ -70,12 +64,12 @@
70
64
  }
71
65
 
72
66
  /* 标题后的段落减少间距 */
73
- .j-content h1 + p,
74
- .j-content h2 + p,
75
- .j-content h3 + p,
76
- .j-content h4 + p,
77
- .j-content h5 + p,
78
- .j-content h6 + p {
67
+ .j-content h1+p,
68
+ .j-content h2+p,
69
+ .j-content h3+p,
70
+ .j-content h4+p,
71
+ .j-content h5+p,
72
+ .j-content h6+p {
79
73
  margin-top: calc(var(--content-paragraph-space) * -0.5);
80
74
  }
81
75
 
@@ -255,7 +249,7 @@
255
249
  color: var(--tone-body);
256
250
  }
257
251
 
258
- .j-content dd + dt {
252
+ .j-content dd+dt {
259
253
  margin-top: 1em;
260
254
  }
261
255
 
@@ -283,7 +277,7 @@
283
277
  margin: 0;
284
278
  }
285
279
 
286
- .j-content blockquote p + p {
280
+ .j-content blockquote p+p {
287
281
  margin-top: 0.75em;
288
282
  }
289
283
 
@@ -399,7 +393,7 @@
399
393
  padding: 1.25em 1.5em;
400
394
  }
401
395
 
402
- .j-content pre.j-code-editor .code-header + code[class*='language-'] {
396
+ .j-content pre.j-code-editor .code-header+code[class*='language-'] {
403
397
  padding: 0.5rem 1rem 1rem;
404
398
  }
405
399
 
@@ -647,14 +641,14 @@
647
641
  transition: transform 0.2s ease;
648
642
  }
649
643
 
650
- .j-content details[open] > summary::before {
644
+ .j-content details[open]>summary::before {
651
645
  transform: rotate(90deg);
652
646
  }
653
647
 
654
648
  .j-content details[open] p,
655
649
  .j-content details[open] ul,
656
650
  .j-content details[open] ol,
657
- .j-content details[open] > * {
651
+ .j-content details[open]>* {
658
652
  margin-bottom: 1em;
659
653
  }
660
654
 
@@ -662,11 +656,11 @@
662
656
  margin-bottom: 0.75em;
663
657
  } */
664
658
 
665
- .j-content details[open] > summary + * {
659
+ .j-content details[open]>summary+* {
666
660
  margin-top: 0;
667
661
  }
668
662
 
669
- .j-content details[open] > *:last-child {
663
+ .j-content details[open]>*:last-child {
670
664
  margin-bottom: 0;
671
665
  }
672
666
 
@@ -711,11 +705,11 @@
711
705
  }
712
706
 
713
707
  /* First/Last child margin reset */
714
- .j-content > *:first-child {
708
+ .j-content>*:first-child {
715
709
  margin-top: 0;
716
710
  }
717
711
 
718
- .j-content > *:last-child {
712
+ .j-content>*:last-child {
719
713
  margin-bottom: 0;
720
714
  }
721
- }
715
+ }
package/src/css/drop.css CHANGED
@@ -24,5 +24,8 @@
24
24
  background: var(--drop-bg);
25
25
  color: var(--drop-fg);
26
26
  box-shadow: var(--drop-box-shadow);
27
+ position: relative;
28
+ min-width: 64px;
29
+ min-height: 32px;
27
30
  }
28
- }
31
+ }
package/src/css/form.css CHANGED
@@ -67,6 +67,14 @@
67
67
  .j-form .form-buttons {
68
68
  display: flex;
69
69
  gap: var(--form-buttons-gap, var(--s2));
70
+ margin-top: 1rem;
71
+ }
72
+
73
+ .j-form [data-form-buttons] button {
74
+ position: relative;
75
+ }
76
+
77
+ .j-form.is-horizontal .form-buttons {
70
78
  margin: 0;
71
79
  }
72
80