zabi-components 5.0.10 → 5.0.12

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.
@@ -39,21 +39,24 @@
39
39
  ? "px-4 py-2 text-base"
40
40
  : "px-3 py-1 text-sm"; // default md
41
41
 
42
- // Variant classes - using semantic color system
42
+ // Variant classes - using semantic color system with improved WCAG contrast
43
+ // Using colored backgrounds with white text for better contrast ratios (4.5:1+)
44
+ // Using darker shades for warning and energetic to ensure WCAG AA compliance
45
+ // Note: Using text-white instead of text-inverse to ensure light text in both light and dark modes
43
46
  const variantClass =
44
47
  variant === "success"
45
- ? "bg-surface-level-1 border-success text-success"
48
+ ? "bg-[var(--color-success)] border-[var(--color-success)] text-white"
46
49
  : variant === "warning"
47
- ? "bg-surface-level-1 border-secondary text-warning"
50
+ ? "bg-[var(--color-warning-weak)] border-[var(--color-warning-weak)] text-white"
48
51
  : variant === "error"
49
- ? "bg-surface-level-1 border-secondary text-error"
52
+ ? "bg-[var(--color-error)] border-[var(--color-error)] text-white"
50
53
  : variant === "info"
51
- ? "bg-surface-level-1 border-secondary text-info"
54
+ ? "bg-[var(--color-info)] border-[var(--color-info)] text-white"
52
55
  : variant === "neutral"
53
- ? "bg-surface-level-1 border-secondary text-neutral"
56
+ ? "bg-[var(--color-neutral)] border-[var(--color-neutral)] text-white"
54
57
  : variant === "energetic"
55
- ? "bg-surface-level-1 border-secondary text-energetic"
56
- : "bg-surface-level-1 border-secondary text-body"; // default
58
+ ? "bg-[var(--color-energetic-weak)] border-[var(--color-energetic-weak)] text-white"
59
+ : "bg-[var(--color-secondary)] border-[var(--color-secondary)] text-white"; // default
57
60
 
58
61
  return `${baseClasses} ${sizeClass} ${variantClass}`.trim();
59
62
  });
@@ -72,19 +75,19 @@
72
75
  <span class={classes()}>
73
76
  {#if showIcon}
74
77
  {#if variant === "success"}
75
- <Check size={iconSize()} class={iconSpacingClass()} />
78
+ <Check size={iconSize()} class="{iconSpacingClass()} text-white" />
76
79
  {:else if variant === "warning"}
77
- <AlertTriangle size={iconSize()} class={iconSpacingClass()} />
80
+ <AlertTriangle size={iconSize()} class="{iconSpacingClass()} text-white" />
78
81
  {:else if variant === "error"}
79
- <X size={iconSize()} class={iconSpacingClass()} />
82
+ <X size={iconSize()} class="{iconSpacingClass()} text-white" />
80
83
  {:else if variant === "info"}
81
- <Info size={iconSize()} class={iconSpacingClass()} />
84
+ <Info size={iconSize()} class="{iconSpacingClass()} text-white" />
82
85
  {:else if variant === "neutral"}
83
- <Info size={iconSize()} class={iconSpacingClass()} />
86
+ <Info size={iconSize()} class="{iconSpacingClass()} text-white" />
84
87
  {:else if variant === "energetic"}
85
- <Info size={iconSize()} class={iconSpacingClass()} />
88
+ <Info size={iconSize()} class="{iconSpacingClass()} text-white" />
86
89
  {:else}
87
- <Info size={iconSize()} class={iconSpacingClass()} />
90
+ <Info size={iconSize()} class="{iconSpacingClass()} text-white" />
88
91
  {/if}
89
92
  {/if}
90
93
  {text}
@@ -22,34 +22,62 @@
22
22
  ...restProps
23
23
  }: Props = $props();
24
24
 
25
- // Size classes using full class names
25
+ // Size classes matching M3 design specifications
26
26
  const sizeClass = $derived(() => {
27
- return size === "sm"
28
- ? "px-3 py-1.5 text-sm font-medium"
29
- : size === "lg"
30
- ? "px-6 py-3 text-base font-semibold"
31
- : "px-4 py-2 text-sm font-medium"; // default md
27
+ if (size === "sm") {
28
+ return {
29
+ padding: "px-4 py-2.5",
30
+ text: "text-sm",
31
+ font: "font-medium",
32
+ leading: "leading-5",
33
+ tracking: "tracking-[0.1px]",
34
+ radius: "rounded-lg",
35
+ gap: "gap-2"
36
+ };
37
+ } else if (size === "lg") {
38
+ return {
39
+ padding: "px-12 py-8",
40
+ text: "text-2xl",
41
+ font: "font-normal",
42
+ leading: "leading-8",
43
+ tracking: "tracking-normal",
44
+ radius: "rounded-2xl",
45
+ gap: "gap-3"
46
+ };
47
+ } else {
48
+ // default md
49
+ return {
50
+ padding: "px-6 py-4",
51
+ text: "text-base",
52
+ font: "font-medium",
53
+ leading: "leading-6",
54
+ tracking: "tracking-[0.15px]",
55
+ radius: "rounded-lg",
56
+ gap: "gap-2"
57
+ };
58
+ }
32
59
  });
33
60
 
34
61
  // Variant classes using semantic action colors
35
62
  const variantClass = $derived(() => {
36
63
  return variant === "primary"
37
- ? "bg-action-primary text-inverse focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
64
+ ? "bg-action-primary text-inverse hover:bg-action-primary-hover active:bg-action-primary-active focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
38
65
  : variant === "secondary"
39
- ? "border border-secondary text-description hover:border-secondary-hover focus:ring-2 focus:ring-stone-500 focus:ring-offset-2"
66
+ ? "bg-brand-100 text-brand-800 hover:bg-brand-200 active:bg-brand-300 focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"
40
67
  : variant === "danger"
41
- ? "bg-action-danger text-inverse focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
68
+ ? "bg-action-danger text-inverse hover:bg-action-danger-hover active:bg-action-danger-active focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
42
69
  : variant === "ghost"
43
70
  ? "bg-transparent text-headline hover:bg-surface-hover active:bg-surface-active focus:ring-2 focus:ring-stone-500 focus:ring-offset-2 disabled:text-disabled"
44
- : "bg-action-primary text-inverse focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"; // default primary
71
+ : "bg-action-primary text-inverse hover:bg-action-primary-hover active:bg-action-primary-active focus:ring-2 focus:ring-brand-500 focus:ring-offset-2"; // default primary
45
72
  });
46
73
 
47
- // Button classes using Badge pattern
74
+ // Button classes matching M3 design pattern
48
75
  const buttonClasses = $derived(() => {
76
+ const sizeStyles = sizeClass();
49
77
  const baseClasses =
50
- "inline-flex items-center justify-center rounded-md transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none focus:ring-2 focus:ring-offset-2 active:scale-95 transform-gpu";
78
+ "inline-flex items-center justify-center transition-all duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none focus:outline-none";
51
79
 
52
- return `${baseClasses} ${sizeClass()} ${variantClass()}`.trim();
80
+ return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.font} ${sizeStyles.leading} ${sizeStyles.tracking} ${sizeStyles.radius} ${sizeStyles.gap} ${variantClass()}`.trim();
53
81
  });
54
82
  </script>
55
83
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  interface Props {
5
5
  title?: string;
6
+ description?: string;
6
7
  image?: string;
7
8
  onclick?: (event: MouseEvent) => void | Promise<void>;
8
9
  size?: "sm" | "md" | "lg";
@@ -12,6 +13,7 @@
12
13
 
13
14
  let {
14
15
  title = "",
16
+ description = "",
15
17
  image = "",
16
18
  onclick,
17
19
  size = "md",
@@ -21,23 +23,41 @@
21
23
  }: Props = $props();
22
24
 
23
25
  const sizeClass = $derived(() => {
24
- return size === "sm" ? "p-3" : size === "lg" ? "p-6" : "p-4"; // default md
26
+ return size === "sm" ? "p-4" : size === "lg" ? "p-8" : "p-6"; // default md
25
27
  });
26
28
 
27
29
  const cardClasses = $derived(() => {
28
30
  const baseClasses =
29
- "rounded-lg transition-all duration-200 min-w-64 bg-surface-level-0";
31
+ "rounded-lg transition-all duration-200 min-w-64 bg-surface-elevated shadow-sm";
30
32
  const interactiveClasses = onclick
31
- ? "cursor-pointer hover:shadow-adaptive-md hover:bg-surface-hover hover:border-focus"
33
+ ? "cursor-pointer hover:shadow-lg hover:bg-surface-hover"
32
34
  : "";
33
35
  const widthClasses = fullWidth ? "w-full" : "";
34
36
 
35
37
  return `${baseClasses} ${interactiveClasses} ${widthClasses} ${sizeClass()}`.trim();
36
38
  });
37
39
 
38
- const titleClasses = $derived(
39
- () => "text-lg font-semibold mb-2 text-headline",
40
- );
40
+ const titleClasses = $derived(() => {
41
+ if (size === "sm") {
42
+ return "text-lg font-medium mb-2 text-headline";
43
+ } else if (size === "lg") {
44
+ return "text-2xl font-medium mb-4 text-headline";
45
+ } else {
46
+ // default md
47
+ return "text-xl font-medium mb-3 text-headline";
48
+ }
49
+ });
50
+
51
+ const descriptionClasses = $derived(() => {
52
+ if (size === "sm") {
53
+ return "text-xs text-description mb-3";
54
+ } else if (size === "lg") {
55
+ return "text-base text-description mb-5";
56
+ } else {
57
+ // default md
58
+ return "text-sm text-description mb-4";
59
+ }
60
+ });
41
61
  </script>
42
62
 
43
63
  <div class={cardClasses()} {onclick} {...restProps}>
@@ -45,7 +65,7 @@
45
65
  <img
46
66
  src={image}
47
67
  alt={title}
48
- class="w-full h-48 object-cover rounded-md mb-4"
68
+ class="w-full h-48 object-cover rounded-lg mb-4"
49
69
  />
50
70
  {/if}
51
71
 
@@ -53,6 +73,10 @@
53
73
  <h3 class={titleClasses()}>{title}</h3>
54
74
  {/if}
55
75
 
76
+ {#if description}
77
+ <p class={descriptionClasses()}>{description}</p>
78
+ {/if}
79
+
56
80
  {#if children}
57
81
  {@render children()}
58
82
  {/if}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from "svelte";
2
2
  interface Props {
3
3
  title?: string;
4
+ description?: string;
4
5
  image?: string;
5
6
  onclick?: (event: MouseEvent) => void | Promise<void>;
6
7
  size?: "sm" | "md" | "lg";
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { onMount } from "svelte";
3
2
  // SSR-safe ID generation
4
3
  function generateId(prefix: string = "id"): string {
5
4
  if (typeof window !== "undefined") {
@@ -22,39 +21,82 @@
22
21
  name = "",
23
22
  disabled = false,
24
23
  label = "",
24
+ onchange,
25
25
  ...restProps
26
26
  }: Props = $props();
27
27
 
28
28
  // Generate unique ID - SSR safe (call directly, not in $state)
29
29
  const checkboxId = generateId("checkbox");
30
30
 
31
- // Checkbox classes using Badge pattern
32
- const checkboxClasses = $derived(
33
- () =>
34
- "w-4 h-4 text-brand-600 bg-gray-100 border-gray-300 rounded focus:ring-brand-500 focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed",
35
- );
31
+ // Checkbox container classes matching M3 design
32
+ const checkboxContainerClasses = $derived(() => {
33
+ const baseClasses = "relative inline-flex items-center justify-center w-5 h-5 rounded transition-all duration-200";
34
+
35
+ if (disabled) {
36
+ return `${baseClasses} opacity-50 cursor-not-allowed`;
37
+ }
38
+ return `${baseClasses} cursor-pointer`;
39
+ });
40
+
41
+ // Checkbox border and background classes matching M3 design
42
+ const checkboxBoxClasses = $derived(() => {
43
+ if (checked) {
44
+ return disabled
45
+ ? "border-0 bg-brand-300"
46
+ : "border-0 bg-brand-600";
47
+ }
48
+ return disabled
49
+ ? "border-2 border-stone-400 bg-transparent"
50
+ : "border-2 border-stone-400 bg-transparent";
51
+ });
36
52
 
37
53
  function handleChange(event: Event) {
54
+ if (disabled) return;
38
55
  const target = event.target as HTMLInputElement;
39
56
  checked = target.checked;
57
+ if (onchange) {
58
+ onchange(event);
59
+ }
40
60
  }
41
61
  </script>
42
62
 
43
63
  <div class="flex items-center gap-2">
44
- <input
45
- type="checkbox"
46
- id={checkboxId}
47
- {name}
48
- {checked}
49
- {disabled}
50
- class={checkboxClasses()}
51
- onchange={handleChange}
52
- {...restProps}
53
- />
54
- {#if label}
55
- <label
56
- for={checkboxId}
57
- class="text-sm font-medium cursor-pointer text-label">{label}</label
58
- >
59
- {/if}
64
+ <label
65
+ for={checkboxId}
66
+ class="flex items-center gap-2 cursor-pointer {disabled ? 'opacity-50 cursor-not-allowed' : ''}"
67
+ >
68
+ <div class={checkboxContainerClasses()}>
69
+ <input
70
+ type="checkbox"
71
+ id={checkboxId}
72
+ {name}
73
+ {checked}
74
+ {disabled}
75
+ class="sr-only"
76
+ onchange={handleChange}
77
+ {...restProps}
78
+ />
79
+ <div class="absolute inset-0 {checkboxBoxClasses()} rounded"></div>
80
+ {#if checked}
81
+ <svg
82
+ class="absolute w-3 h-3 text-white pointer-events-none z-10"
83
+ fill="none"
84
+ stroke="currentColor"
85
+ viewBox="0 0 24 24"
86
+ >
87
+ <path
88
+ stroke-linecap="round"
89
+ stroke-linejoin="round"
90
+ stroke-width="2.5"
91
+ d="M5 13l4 4L19 7"
92
+ />
93
+ </svg>
94
+ {/if}
95
+ </div>
96
+ {#if label}
97
+ <span class="text-sm font-medium text-label">
98
+ {label}
99
+ </span>
100
+ {/if}
101
+ </label>
60
102
  </div>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
3
+
2
4
  // SSR-safe ID generation
3
5
  function generateId(prefix: string = "id"): string {
4
6
  if (typeof window !== "undefined") {
@@ -17,6 +19,7 @@
17
19
  disabled?: boolean;
18
20
  size?: "sm" | "md" | "lg";
19
21
  variant?: "default" | "success" | "warning" | "error";
22
+ message?: string;
20
23
  oninput?: (event: Event) => void;
21
24
  }
22
25
 
@@ -29,6 +32,7 @@
29
32
  disabled = false,
30
33
  size = "md",
31
34
  variant = "default",
35
+ message = "",
32
36
  oninput,
33
37
  ...restProps
34
38
  }: Props = $props();
@@ -36,13 +40,28 @@
36
40
  // Generate unique ID - SSR safe (call directly, not in $state)
37
41
  const inputId = generateId("input");
38
42
 
39
- // Size classes using full class names
43
+ // Size classes matching M3 design specifications
40
44
  const sizeClass = $derived(() => {
41
- return size === "sm"
42
- ? "px-3 py-1.5 text-sm"
43
- : size === "lg"
44
- ? "px-5 py-3 text-base"
45
- : "px-4 py-2 text-sm"; // default md
45
+ if (size === "sm") {
46
+ return {
47
+ padding: "px-4 py-2",
48
+ text: "text-sm",
49
+ leading: "leading-5"
50
+ };
51
+ } else if (size === "lg") {
52
+ return {
53
+ padding: "px-4 py-3",
54
+ text: "text-base",
55
+ leading: "leading-6"
56
+ };
57
+ } else {
58
+ // default md
59
+ return {
60
+ padding: "px-4 py-2.5",
61
+ text: "text-base",
62
+ leading: "leading-6"
63
+ };
64
+ }
46
65
  });
47
66
 
48
67
  // Variant classes using semantic colors
@@ -53,15 +72,16 @@
53
72
  ? "border-warning focus:border-warning focus:ring-warning"
54
73
  : variant === "error"
55
74
  ? "border-error focus:border-error focus:ring-error"
56
- : "border-border focus:border-focus focus:ring-focus"; // default
75
+ : "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
57
76
  });
58
77
 
59
- // Input classes using Badge pattern
78
+ // Input classes matching M3 design
60
79
  const inputClasses = $derived(() => {
80
+ const sizeStyles = sizeClass();
61
81
  const baseClasses =
62
- "w-full border rounded-md transition-colors duration-200 placeholder:text-input-placeholder focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-surface disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-surface-disabled";
82
+ "w-full bg-brand-100 rounded-lg transition-all duration-200 placeholder:text-description text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed";
63
83
 
64
- return `${baseClasses} ${sizeClass()} ${variantClass()}`.trim();
84
+ return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
65
85
  });
66
86
 
67
87
  // Label classes using semantic text colors
@@ -69,6 +89,26 @@
69
89
  () => "block text-sm font-medium text-label mb-1",
70
90
  );
71
91
 
92
+ // Message classes based on variant
93
+ const messageClasses = $derived(() => {
94
+ if (variant === "error") {
95
+ return "text-error text-sm mt-1 flex items-center gap-1.5";
96
+ } else if (variant === "success") {
97
+ return "text-success text-sm mt-1 flex items-center gap-1.5";
98
+ } else if (variant === "warning") {
99
+ return "text-warning text-sm mt-1 flex items-center gap-1.5";
100
+ }
101
+ return "text-description text-sm mt-1 flex items-center gap-1.5";
102
+ });
103
+
104
+ // Get icon component based on variant
105
+ const getIcon = $derived(() => {
106
+ if (variant === "error") return AlertCircle;
107
+ if (variant === "success") return CheckCircle;
108
+ if (variant === "warning") return AlertTriangle;
109
+ return null;
110
+ });
111
+
72
112
  function handleInput(event: Event) {
73
113
  const target = event.target as HTMLInputElement;
74
114
  value = target.value;
@@ -93,6 +133,17 @@
93
133
  {disabled}
94
134
  class={inputClasses()}
95
135
  oninput={handleInput}
136
+ aria-invalid={variant === "error" ? "true" : undefined}
137
+ aria-describedby={message ? `${inputId}-message` : undefined}
96
138
  {...restProps}
97
139
  />
140
+ {#if message && variant !== "default"}
141
+ <p id={`${inputId}-message`} class={messageClasses()} role="alert">
142
+ {#if getIcon()}
143
+ {@const Icon = getIcon()}
144
+ <Icon size={14} class="shrink-0" />
145
+ {/if}
146
+ <span>{message}</span>
147
+ </p>
148
+ {/if}
98
149
  </div>
@@ -7,6 +7,7 @@ interface Props {
7
7
  disabled?: boolean;
8
8
  size?: "sm" | "md" | "lg";
9
9
  variant?: "default" | "success" | "warning" | "error";
10
+ message?: string;
10
11
  oninput?: (event: Event) => void;
11
12
  }
12
13
  declare const Input: import("svelte").Component<Props, {}, "">;