@quaffui/quaff 1.0.0-alpha1 → 1.0.0-alpha2

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Quaff is a component library for Svelte that follows the [Material Design 3](https://m3.material.io/) guidelines. It provides a comprehensive set of UI components designed to create beautiful, consistent, and accessible web applications.
4
4
 
5
- > **⚠️ Early Development Notice:** Quaff is currently in pre-alpha stage (v0.1.0-prealpha). APIs may change significantly between versions, and components might be incomplete or contain bugs. Use in production at your own risk.
5
+ > **⚠️ Early Development Notice:** Quaff is still maturing and not battle-tested yet. Use it in production if you're feeling brave! We welcome contributions and feedback to help shape the library.
6
6
 
7
7
  ## Overview
8
8
 
@@ -1,5 +1,17 @@
1
1
  declare class Quaff {
2
2
  version: string;
3
+ private static readonly breakpointList;
4
+ readonly breakpoints: {
5
+ currentWidth: number;
6
+ current: "xs" | "sm" | "md" | "lg" | "xl";
7
+ isMoreThan(breakpoint: "xs" | "sm" | "md" | "lg" | "xl", included?: boolean): boolean;
8
+ isLessThan(breakpoint: "xs" | "sm" | "md" | "lg" | "xl", included?: boolean): boolean;
9
+ xs: number;
10
+ sm: number;
11
+ md: number;
12
+ lg: number;
13
+ xl: number;
14
+ };
3
15
  router: import("@sveltejs/kit").Page<Record<string, string>, string | null>;
4
16
  protected dark: boolean;
5
17
  init(): void;
@@ -1,8 +1,54 @@
1
1
  import { onMount } from "svelte";
2
+ import { innerWidth } from "svelte/reactivity/window";
2
3
  import { version } from "../helpers";
3
4
  import { page } from "$app/state";
4
5
  class Quaff {
5
6
  version = version;
7
+ static breakpointList = {
8
+ xs: 0,
9
+ sm: 600,
10
+ md: 960,
11
+ lg: 1280,
12
+ xl: 1920,
13
+ };
14
+ breakpoints = $derived.by(() => {
15
+ const currentWidth = innerWidth.current;
16
+ let current;
17
+ if (!currentWidth || currentWidth < 600) {
18
+ current = "xs";
19
+ }
20
+ else if (currentWidth < 960) {
21
+ current = "sm";
22
+ }
23
+ else if (currentWidth < 1280) {
24
+ current = "md";
25
+ }
26
+ else if (currentWidth < 1920) {
27
+ current = "lg";
28
+ }
29
+ else {
30
+ current = "xl";
31
+ }
32
+ return {
33
+ ...Quaff.breakpointList,
34
+ currentWidth: currentWidth || 0,
35
+ current,
36
+ isMoreThan(breakpoint, included = false) {
37
+ if (!currentWidth) {
38
+ return false;
39
+ }
40
+ const breakpointWidth = this[breakpoint];
41
+ return included ? currentWidth >= breakpointWidth : currentWidth > breakpointWidth;
42
+ },
43
+ isLessThan(breakpoint, included = false) {
44
+ if (!currentWidth) {
45
+ return false;
46
+ }
47
+ const breakpointWidth = this[breakpoint];
48
+ return included ? currentWidth <= breakpointWidth : currentWidth < breakpointWidth;
49
+ },
50
+ };
51
+ });
6
52
  router = $derived(page);
7
53
  dark = $state(false);
8
54
  init() {
@@ -5,6 +5,9 @@
5
5
  border-radius: 0.75rem;
6
6
  transition: transform var(--speed-3) padding var(--speed-3) border-radius var(--speed-3);
7
7
 
8
+ background-color: var(--surface);
9
+ color: var(--on-surface);
10
+
8
11
  @include mixins.padding("a-md");
9
12
  @include mixins.elevate(1, "bottom");
10
13
 
@@ -22,4 +25,23 @@
22
25
  &--rounded {
23
26
  border-radius: 2rem;
24
27
  }
28
+
29
+ &--fill {
30
+ background-color: var(--surface-variant);
31
+
32
+ &.q-card--primary {
33
+ background-color: var(--primary-container);
34
+ color: var(--on-primary-container);
35
+ }
36
+
37
+ &.q-card--secondary {
38
+ background-color: var(--secondary-container);
39
+ color: var(--on-secondary-container);
40
+ }
41
+
42
+ &.q-card--tertiary {
43
+ background-color: var(--tertiary-container);
44
+ color: var(--on-tertiary-container);
45
+ }
46
+ }
25
47
  }
@@ -10,14 +10,21 @@
10
10
  ...props
11
11
  }: QCardProps = $props();
12
12
 
13
- const colorOptions: (typeof fill)[] = ["primary", "secondary", "tertiary"];
13
+ type ColorOptions = "primary" | "secondary" | "tertiary";
14
+
15
+ const colorOptions: ColorOptions[] = [
16
+ "primary",
17
+ "secondary",
18
+ "tertiary",
19
+ ] as const;
14
20
 
15
21
  const color = $derived.by(() => {
16
22
  if (fill) {
17
- return colorOptions.includes(fill)
18
- ? `${fill}-container`
19
- : "surface-variant";
23
+ return fill === true || !colorOptions.includes(fill as ColorOptions)
24
+ ? "surface-variant"
25
+ : fill;
20
26
  }
27
+
21
28
  return "surface";
22
29
  });
23
30
  </script>
@@ -26,11 +33,12 @@
26
33
  {...props}
27
34
  class={[
28
35
  "q-card",
29
- color,
30
36
  props.class,
31
37
  flat && "q-card--flat",
32
38
  bordered && "q-card--bordered",
33
39
  rounded && "q-card--rounded",
40
+ fill && "q-card--fill",
41
+ fill && color !== "surface" && `q-card--${color}`,
34
42
  ]}
35
43
  data-quaff
36
44
  >
@@ -73,12 +73,12 @@
73
73
  <div {...props} class="q-code-block" data-quaff>
74
74
  {#if copiable}
75
75
  <div
76
- class="flex justify-between {title
76
+ class="q-code-block__title-section justify-between {title
77
77
  ? 'items-center'
78
78
  : 'justify-end'} q-pb-sm"
79
79
  >
80
80
  {#if title}
81
- <h4 class="q-ma-none q-pr-lg">{title}</h4>
81
+ <h4 class="q-ma-none">{title}</h4>
82
82
  {/if}
83
83
  <QBtn
84
84
  class="border-{btnColor} text-{btnColor}"
@@ -106,11 +106,24 @@
106
106
  <style>
107
107
  .q-code-block {
108
108
  border-radius: inherit;
109
+ }
110
+ .q-code-block :global(pre) {
111
+ text-align: left;
112
+ padding: 1rem;
113
+ overflow: auto;
114
+ }
109
115
 
110
- :global(pre) {
111
- text-align: left;
112
- padding: 1rem;
113
- overflow: auto;
116
+ .q-code-block__title-section {
117
+ display: flex;
118
+ gap: 0.5rem;
119
+ }
120
+
121
+ @media only screen and (max-width: 599px) {
122
+ :global(.q-dialog:has(.q-code-block__title-section)) {
123
+ min-width: auto;
124
+ }
125
+ .q-code-block__title-section {
126
+ flex-direction: column;
114
127
  }
115
128
  }
116
129
  </style>
@@ -21,6 +21,15 @@
21
21
  z-index: 6;
22
22
  }
23
23
 
24
+ &__swipearea {
25
+ position: absolute;
26
+ top: 0;
27
+ bottom: 0;
28
+ z-index: 2;
29
+
30
+ touch-action: none;
31
+ }
32
+
24
33
  @each $side in ("left", "right") {
25
34
  &.q-drawer--#{$side} {
26
35
  #{$side}: 0;
@@ -37,3 +46,10 @@
37
46
  transform: translate(0);
38
47
  }
39
48
  }
49
+
50
+ @each $side in ("left", "right") {
51
+ .q-drawer__swipearea--#{$side} {
52
+ #{$side}: 0;
53
+ width: 1.25rem;
54
+ }
55
+ }
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
- import { getContext, onDestroy, onMount, untrack } from "svelte";
2
+ import { getContext, onMount, untrack } from "svelte";
3
+ import { on } from "svelte/events";
3
4
  import { navigating } from "$app/state";
4
5
  import { useSize } from "../../composables";
5
6
  import { QContext } from "../../classes/QContext.svelte";
@@ -15,11 +16,27 @@
15
16
  bordered = false,
16
17
  overlay = false,
17
18
  persistent = false,
19
+ noSwipe = false,
20
+ swipeThreshold = "30%",
18
21
  children,
19
22
  ...props
20
23
  }: QDrawerProps = $props();
21
24
 
25
+ const PEEK_THRESHOLD = 30; // How far the drawer peeks out when cursor is near the edge
26
+ const TRANSITION = "top 0.3s, bottom 0.3s, transform 0.3s";
27
+
28
+ let unlistenClick: () => void;
29
+ let unlistenPointerdown: () => void;
30
+ let unlistenPointermove: () => void;
31
+ let unlistenPointerup: () => void;
32
+ let unlistenPointercancel: () => void;
33
+
22
34
  let drawerEl = $state<HTMLDivElement>();
35
+ let swipeAreaEl = $state<HTMLDivElement>();
36
+
37
+ let isSwiping = $state(false);
38
+ let startX = $state(0);
39
+ let dragOffset = $state(0);
23
40
 
24
41
  const drawerContext = QContext.get<DrawerContext>(
25
42
  QLayoutCtxName.drawer[side],
@@ -61,13 +78,26 @@
61
78
 
62
79
  onMount(() => {
63
80
  setTimeout(() => {
64
- if (drawerEl) {
65
- drawerEl.style.transition = "top 0.3s, bottom 0.3s, transform 0.3s";
66
- }
81
+ drawerEl?.style.setProperty("transition", TRANSITION);
67
82
  }, 100);
68
83
 
69
84
  return () => {
70
- window.removeEventListener("click", tryClose);
85
+ unlistenClick?.();
86
+ unlistenPointerdown?.();
87
+
88
+ if (isSwiping) {
89
+ unlistenPointermove?.();
90
+ unlistenPointerup?.();
91
+ unlistenPointercancel?.();
92
+
93
+ resetBodyStyles();
94
+ }
95
+
96
+ drawerContext?.updateEntries({
97
+ width: 0,
98
+ takesSpace: false,
99
+ ready: false,
100
+ });
71
101
  };
72
102
  });
73
103
 
@@ -80,19 +110,27 @@
80
110
  $effect(() => {
81
111
  if (value) {
82
112
  setTimeout(() => {
83
- window.addEventListener("click", tryClose);
113
+ unlistenClick = on(window, "click", tryClose);
84
114
  }, 150);
115
+
116
+ untrack(() => {
117
+ if (!noSwipe && !persistent) {
118
+ unlistenPointerdown = on(drawerEl!, "pointerdown", handlePointerDown);
119
+ swipeAreaEl?.style.setProperty("z-index", "-1");
120
+ }
121
+ });
85
122
  } else {
86
- window.removeEventListener("click", tryClose);
87
- }
88
- });
123
+ unlistenClick?.();
89
124
 
90
- onDestroy(() => {
91
- drawerContext?.updateEntries({
92
- width: 0,
93
- takesSpace: false,
94
- ready: false,
95
- });
125
+ if (!noSwipe) {
126
+ unlistenPointerdown = on(
127
+ swipeAreaEl!,
128
+ "pointerdown",
129
+ handlePointerDown,
130
+ );
131
+ swipeAreaEl?.style.setProperty("z-index", "10");
132
+ }
133
+ }
96
134
  });
97
135
 
98
136
  $effect(() => {
@@ -125,6 +163,158 @@
125
163
  hide();
126
164
  }
127
165
  }
166
+
167
+ function handlePointerDown(e: PointerEvent) {
168
+ if (
169
+ noSwipe ||
170
+ !drawerEl ||
171
+ !swipeAreaEl ||
172
+ (e.pointerType === "mouse" && e.buttons !== 1) ||
173
+ (value && persistent)
174
+ ) {
175
+ return;
176
+ }
177
+
178
+ const drawerRect = drawerEl.getBoundingClientRect();
179
+ const y = e.clientY;
180
+
181
+ if (y < drawerRect.top || y > drawerRect.bottom) {
182
+ // Ignore pointer events outside the vertical bounds of the drawer
183
+ return;
184
+ }
185
+
186
+ let swipeAllowed = false;
187
+ startX = e.clientX;
188
+
189
+ if (!value) {
190
+ swipeAllowed = true;
191
+
192
+ const baseWidth = side === "left" ? -width : width;
193
+
194
+ dragOffset =
195
+ baseWidth + (side === "left" ? PEEK_THRESHOLD : -PEEK_THRESHOLD);
196
+
197
+ drawerEl.style.transform = `translateX(${dragOffset}px)`;
198
+ } else {
199
+ // If drawer is open, allow swipe from anywhere
200
+ swipeAllowed = true;
201
+
202
+ // No initial dragOffset change or transform needed as it's already open.
203
+ // dragOffset will be calculated fresh in handlePointerMove.
204
+ }
205
+
206
+ if (!swipeAllowed) {
207
+ return;
208
+ }
209
+
210
+ isSwiping = true;
211
+
212
+ e.stopPropagation();
213
+ e.preventDefault();
214
+
215
+ document.body.style.setProperty("cursor", "grabbing");
216
+ document.body.style.setProperty("user-select", "none"); // Disable text selection
217
+
218
+ drawerEl.style.transition = "none"; // Disable CSS transitions for smooth dragging
219
+ drawerEl.style.touchAction = "none"; // Disable touch actions
220
+
221
+ swipeAreaEl?.style.setProperty("width", "100vw"); // Expand swipe area to full width
222
+
223
+ unlistenPointermove = on(
224
+ e.target as HTMLElement,
225
+ "pointermove",
226
+ handlePointerMove,
227
+ );
228
+ unlistenPointerup = on(
229
+ e.target as HTMLElement,
230
+ "pointerup",
231
+ handlePointerUp,
232
+ {
233
+ passive: true,
234
+ },
235
+ );
236
+ unlistenPointercancel = on(
237
+ e.target as HTMLElement,
238
+ "pointercancel",
239
+ handlePointerUp,
240
+ {
241
+ passive: true,
242
+ },
243
+ );
244
+ }
245
+
246
+ function handlePointerMove(e: PointerEvent) {
247
+ if (noSwipe || !drawerEl || !swipeAreaEl || !isSwiping) {
248
+ return;
249
+ }
250
+
251
+ e.preventDefault();
252
+
253
+ let deltaX = e.clientX - startX;
254
+
255
+ let newPosition: number;
256
+ // basePosition is the starting translation before applying the current deltaX.
257
+ let basePosition: number;
258
+
259
+ if (side === "left") {
260
+ // For a left-side drawer, dragOffset is between -width (fully closed) and 0 (fully open).
261
+ basePosition = value ? 0 : PEEK_THRESHOLD - width;
262
+ newPosition = basePosition + deltaX;
263
+ // Clamp newPosition to be within [-width, 0]
264
+ dragOffset = Math.max(-width, Math.min(0, newPosition));
265
+ } else {
266
+ // For a right-side drawer, dragOffset is between width (fully closed) and 0 (fully open).
267
+ basePosition = value ? 0 : width - PEEK_THRESHOLD;
268
+ newPosition = basePosition + deltaX;
269
+ // Clamp newPosition to be within [0, width]
270
+ dragOffset = Math.max(0, Math.min(width, newPosition));
271
+ }
272
+
273
+ drawerEl.style.transform = `translateX(${dragOffset}px)`;
274
+ }
275
+
276
+ function handlePointerUp() {
277
+ if (noSwipe || !drawerEl || !swipeAreaEl || !isSwiping) {
278
+ return;
279
+ }
280
+
281
+ isSwiping = false;
282
+
283
+ resetBodyStyles();
284
+
285
+ drawerEl.style.transition = TRANSITION;
286
+ drawerEl.style.transform = "";
287
+ drawerEl.style.touchAction = ""; // Re-enable touch actions
288
+
289
+ swipeAreaEl?.style.removeProperty("width"); // Reset swipe area width
290
+
291
+ const thresholdWidth =
292
+ (width * parseInt(swipeThreshold.replace("%", ""))) / 100;
293
+ const realThreshold = value ? width - thresholdWidth : thresholdWidth;
294
+
295
+ const swiped = width + (side === "left" ? dragOffset : -dragOffset);
296
+
297
+ if (swiped >= realThreshold) {
298
+ if (!value) {
299
+ show(); // Snap open
300
+ }
301
+ } else {
302
+ if (value) {
303
+ hide(); // Snap closed
304
+ }
305
+ }
306
+
307
+ dragOffset = 0;
308
+
309
+ unlistenPointercancel?.();
310
+ unlistenPointermove?.();
311
+ unlistenPointerup?.();
312
+ }
313
+
314
+ function resetBodyStyles() {
315
+ document.body.style.removeProperty("cursor");
316
+ document.body.style.removeProperty("user-select");
317
+ }
128
318
  </script>
129
319
 
130
320
  <div
@@ -145,3 +335,11 @@
145
335
  >
146
336
  {@render children?.()}
147
337
  </div>
338
+
339
+ {#if !noSwipe}
340
+ <div
341
+ bind:this={swipeAreaEl}
342
+ class="q-drawer__swipearea q-drawer__swipearea--{side}"
343
+ onpointerdown={handlePointerDown}
344
+ ></div>
345
+ {/if}
@@ -1,5 +1,5 @@
1
1
  // AUTO GENERATED FILE - DO NOT MODIFY OR DELETE
2
- // @quaffHash fe10bae596729ba8564277a0f6b138de
2
+ // @quaffHash 16a53668c0713b9e0ca53dbe17a8a9de
3
3
  export const QDrawerDocsProps = [
4
4
  {
5
5
  isArray: false,
@@ -125,37 +125,25 @@ export const QDrawerDocsProps = [
125
125
  isArray: false,
126
126
  optional: true,
127
127
  isSnippet: false,
128
- name: "noSwipeOpen",
128
+ name: "noSwipe",
129
129
  type: {
130
130
  name: "boolean",
131
131
  isClickable: false,
132
132
  },
133
- description: "Determines whether swipe gestures can open the drawer. (not supported yet)",
133
+ description: "Determines whether swipe gestures opening the drawer should be disabled or not.",
134
134
  default: "false",
135
135
  },
136
136
  {
137
137
  isArray: false,
138
138
  optional: true,
139
139
  isSnippet: false,
140
- name: "noSwipeClose",
140
+ name: "swipeThreshold",
141
141
  type: {
142
- name: "boolean",
143
- isClickable: false,
144
- },
145
- description: "Determines whether swipe gestures can close the drawer. (not supported yet)",
146
- default: "false",
147
- },
148
- {
149
- isArray: false,
150
- optional: true,
151
- isSnippet: false,
152
- name: "noSwipeBackdrop",
153
- type: {
154
- name: "boolean",
142
+ name: "`${number}%`",
155
143
  isClickable: false,
156
144
  },
157
- description: "Determines whether swipe gestures on the backdrop can close the drawer. (not supported yet)",
158
- default: "false",
145
+ description: "The threshold in percentage of the drawer width that must be swiped for the drawer to snap open/close.\nThis is only applicable if swipe gestures are enabled.",
146
+ default: '"30%"',
159
147
  },
160
148
  ];
161
149
  export const QDrawerDocsSnippets = [];
@@ -54,18 +54,14 @@ export interface QDrawerProps extends NativeProps, HTMLAttributes<HTMLDivElement
54
54
  */
55
55
  persistent?: boolean;
56
56
  /**
57
- * Determines whether swipe gestures can open the drawer. (not supported yet)
57
+ * Determines whether swipe gestures opening the drawer should be disabled or not.
58
58
  * @default false
59
59
  */
60
- noSwipeOpen?: boolean;
60
+ noSwipe?: boolean;
61
61
  /**
62
- * Determines whether swipe gestures can close the drawer. (not supported yet)
63
- * @default false
64
- */
65
- noSwipeClose?: boolean;
66
- /**
67
- * Determines whether swipe gestures on the backdrop can close the drawer. (not supported yet)
68
- * @default false
62
+ * The threshold in percentage of the drawer width that must be swiped for the drawer to snap open/close.
63
+ * This is only applicable if swipe gestures are enabled.
64
+ * @default "30%"
69
65
  */
70
- noSwipeBackdrop?: boolean;
66
+ swipeThreshold?: `${number}%`;
71
67
  }
@@ -172,3 +172,13 @@
172
172
  height: calc(100% - var(--offset-top) - var(--offset-bottom));
173
173
  overflow: auto;
174
174
  }
175
+
176
+ .q-layout > .q-header ~ .q-layout__content {
177
+ border-top-left-radius: 0;
178
+ border-top-right-radius: 0;
179
+ }
180
+
181
+ .q-layout > .q-footer ~ .q-layout__content {
182
+ border-bottom-left-radius: 0;
183
+ border-bottom-right-radius: 0;
184
+ }
@@ -123,16 +123,16 @@
123
123
  (layoutEl && !layoutEl.querySelector(".q-footer"))) &&
124
124
  (!railbarLeft ||
125
125
  leftRailbarCtx.value.ready ||
126
- (layoutEl && !layoutEl.querySelector(".q-railbar--left"))) &&
126
+ (layoutEl && !layoutEl.querySelector(":scope > .q-railbar--left"))) &&
127
127
  (!railbarRight ||
128
128
  rightRailbarCtx.value.ready ||
129
- (layoutEl && !layoutEl.querySelector(".q-railbar--right"))) &&
129
+ (layoutEl && !layoutEl.querySelector(":scope > .q-railbar--right"))) &&
130
130
  (!drawerLeft ||
131
131
  leftDrawerCtx.value.ready ||
132
- (layoutEl && !layoutEl.querySelector(".q-drawer--left"))) &&
132
+ (layoutEl && !layoutEl.querySelector(":scope > .q-drawer--left"))) &&
133
133
  (!drawerRight ||
134
134
  rightDrawerCtx.value.ready ||
135
- (layoutEl && !layoutEl.querySelector(".q-drawer--right"))),
135
+ (layoutEl && !layoutEl.querySelector(":scope > .q-drawer--right"))),
136
136
  );
137
137
 
138
138
  function handleDrawerCtx(ctx: QContext<DrawerContext>) {
@@ -7,6 +7,7 @@
7
7
  color: var(--on-surface-variant);
8
8
 
9
9
  &--headline {
10
+ max-width: 100%;
10
11
  @include mixins.typography("body-large");
11
12
  color: var(--on-surface);
12
13
  }
@@ -222,9 +222,9 @@
222
222
  </QTabs>
223
223
  </div>
224
224
  <QCardSection style="max-height: 416px; overflow-y: auto">
225
- <QList separator bordered style="overflow-x:auto">
225
+ <QList separator bordered>
226
226
  {#each QDocument.docs[api[index]] as doc (doc)}
227
- <QItem>
227
+ <QItem style="overflow-x: auto">
228
228
  <QItemSection type="content">
229
229
  {#snippet headline()}
230
230
  <div