@youcan/celeste 0.23.0 → 0.24.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.
@@ -18,6 +18,8 @@ var button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.d
18
18
  size: { default: "md" },
19
19
  intent: { default: "primary" },
20
20
  variant: { default: "fill" },
21
+ disabled: { type: Boolean },
22
+ loading: { type: Boolean },
21
23
  asChild: { type: Boolean },
22
24
  as: { default: "button" }
23
25
  },
@@ -27,13 +29,17 @@ var button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.d
27
29
  return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(reka_ui.Primitive), {
28
30
  as: __props.as,
29
31
  "as-child": __props.asChild,
30
- class: (0, vue.normalizeClass)((0, vue.unref)(clsx.default)("celeste-button", `celeste-button-intent-${__props.intent}`, `celeste-button-size-${__props.size}`, `celeste-button-variant-${__props.variant}`, props.class))
32
+ disabled: __props.disabled || __props.loading || void 0,
33
+ "aria-busy": __props.loading || void 0,
34
+ class: (0, vue.normalizeClass)((0, vue.unref)(clsx.default)("celeste-button", `celeste-button-intent-${__props.intent}`, `celeste-button-size-${__props.size}`, `celeste-button-variant-${__props.variant}`, __props.loading && !__props.asChild && "celeste-button-loading", props.class))
31
35
  }, {
32
- default: (0, vue.withCtx)(() => [(0, vue.renderSlot)(_ctx.$slots, "default", {}, void 0, true)]),
36
+ default: (0, vue.withCtx)(() => [__props.asChild ? (0, vue.renderSlot)(_ctx.$slots, "default", {}, void 0, true, 0) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [(0, vue.renderSlot)(_ctx.$slots, "default", {}, void 0, true), _cache[0] || (_cache[0] = (0, vue.createElementVNode)("i", { class: "celeste-button-spinner i-celeste-loader-5-line" }, null, -1))], 64))]),
33
37
  _: 3
34
38
  }, 8, [
35
39
  "as",
36
40
  "as-child",
41
+ "disabled",
42
+ "aria-busy",
37
43
  "class"
38
44
  ]);
39
45
  };
@@ -41,7 +47,7 @@ var button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0, vue.d
41
47
  });
42
48
  //#endregion
43
49
  //#region src/components/button/button.vue
44
- var button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-0aeead06"]]);
50
+ var button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-30b82d15"]]);
45
51
  //#endregion
46
52
  Object.defineProperty(exports, "button_default", {
47
53
  enumerable: true,
@@ -42,7 +42,7 @@ var compact_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (
42
42
  });
43
43
  //#endregion
44
44
  //#region src/components/button/compact-button.vue
45
- var compact_button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(compact_button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-bcc27daf"]]);
45
+ var compact_button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(compact_button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-0727facf"]]);
46
46
  //#endregion
47
47
  Object.defineProperty(exports, "compact_button_default", {
48
48
  enumerable: true,
@@ -17,6 +17,8 @@ var fancy_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0,
17
17
  ] },
18
18
  size: { default: "md" },
19
19
  intent: { default: "primary" },
20
+ disabled: { type: Boolean },
21
+ loading: { type: Boolean },
20
22
  asChild: { type: Boolean },
21
23
  as: { default: "button" }
22
24
  },
@@ -26,13 +28,17 @@ var fancy_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0,
26
28
  return (0, vue.openBlock)(), (0, vue.createBlock)((0, vue.unref)(reka_ui.Primitive), {
27
29
  as: __props.as,
28
30
  "as-child": __props.asChild,
29
- class: (0, vue.normalizeClass)((0, vue.unref)(clsx.default)("celeste-fbutton", `celeste-fbutton-${__props.intent}`, `celeste-fbutton-${__props.size}`, props.class))
31
+ disabled: __props.disabled || __props.loading || void 0,
32
+ "aria-busy": __props.loading || void 0,
33
+ class: (0, vue.normalizeClass)((0, vue.unref)(clsx.default)("celeste-fbutton", `celeste-fbutton-${__props.intent}`, `celeste-fbutton-${__props.size}`, __props.loading && !__props.asChild && "celeste-fbutton-loading", props.class))
30
34
  }, {
31
- default: (0, vue.withCtx)(() => [(0, vue.renderSlot)(_ctx.$slots, "default", {}, void 0, true)]),
35
+ default: (0, vue.withCtx)(() => [__props.asChild ? (0, vue.renderSlot)(_ctx.$slots, "default", {}, void 0, true, 0) : ((0, vue.openBlock)(), (0, vue.createElementBlock)(vue.Fragment, { key: 1 }, [(0, vue.renderSlot)(_ctx.$slots, "default", {}, void 0, true), _cache[0] || (_cache[0] = (0, vue.createElementVNode)("i", { class: "celeste-fbutton-spinner i-celeste-loader-5-line" }, null, -1))], 64))]),
32
36
  _: 3
33
37
  }, 8, [
34
38
  "as",
35
39
  "as-child",
40
+ "disabled",
41
+ "aria-busy",
36
42
  "class"
37
43
  ]);
38
44
  };
@@ -40,7 +46,7 @@ var fancy_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0,
40
46
  });
41
47
  //#endregion
42
48
  //#region src/components/button/fancy-button.vue
43
- var fancy_button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(fancy_button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-d53adf47"]]);
49
+ var fancy_button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(fancy_button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-9098298e"]]);
44
50
  //#endregion
45
51
  Object.defineProperty(exports, "fancy_button_default", {
46
52
  enumerable: true,
@@ -34,7 +34,7 @@ var social_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ (0
34
34
  });
35
35
  //#endregion
36
36
  //#region src/components/button/social-button.vue
37
- var social_button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(social_button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-3652f72b"]]);
37
+ var social_button_default = /*#__PURE__*/ require__plugin_vue_export_helper._plugin_vue_export_helper_default(social_button_vue_vue_type_script_setup_true_lang_default, [["__scopeId", "data-v-85544f28"]]);
38
38
  //#endregion
39
39
  Object.defineProperty(exports, "social_button_default", {
40
40
  enumerable: true,
@@ -17,6 +17,8 @@ var text_input_button_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*
17
17
  Array
18
18
  ] },
19
19
  intent: {},
20
+ disabled: { type: Boolean },
21
+ loading: { type: Boolean },
20
22
  asChild: { type: Boolean },
21
23
  as: {},
22
24
  inline: {
@@ -5,10 +5,14 @@ export interface ButtonProps extends PrimitiveProps {
5
5
  size?: 'xxs' | 'xs' | 'sm' | 'md';
6
6
  intent?: 'primary' | 'neutral' | 'error';
7
7
  variant?: 'fill' | 'stroke' | 'lighter' | 'ghost';
8
+ disabled?: boolean;
9
+ loading?: boolean;
8
10
  }
9
- declare var __VLS_8: {};
11
+ declare var __VLS_8: {}, __VLS_10: {};
10
12
  type __VLS_Slots = {} & {
11
13
  default?: (props: typeof __VLS_8) => any;
14
+ } & {
15
+ default?: (props: typeof __VLS_10) => any;
12
16
  };
13
17
  declare const __VLS_base: import('vue').DefineComponent<ButtonProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<ButtonProps> & Readonly<{}>, {
14
18
  size: "xxs" | "xs" | "sm" | "md";
@@ -4,10 +4,14 @@ export interface FancyButtonProps extends PrimitiveProps {
4
4
  class?: HTMLAttributes['class'];
5
5
  size?: 'xs' | 'sm' | 'md';
6
6
  intent?: 'primary' | 'neutral' | 'error' | 'basic';
7
+ disabled?: boolean;
8
+ loading?: boolean;
7
9
  }
8
- declare var __VLS_8: {};
10
+ declare var __VLS_8: {}, __VLS_10: {};
9
11
  type __VLS_Slots = {} & {
10
12
  default?: (props: typeof __VLS_8) => any;
13
+ } & {
14
+ default?: (props: typeof __VLS_10) => any;
11
15
  };
12
16
  declare const __VLS_base: import('vue').DefineComponent<FancyButtonProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<FancyButtonProps> & Readonly<{}>, {
13
17
  size: "xs" | "sm" | "md";
@@ -1,5 +1,5 @@
1
1
  import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper.js";
2
- import { createBlock, defineComponent, normalizeClass, openBlock, renderSlot, unref, withCtx } from "vue";
2
+ import { Fragment, createBlock, createElementBlock, createElementVNode, defineComponent, normalizeClass, openBlock, renderSlot, unref, withCtx } from "vue";
3
3
  import clsx$1 from "clsx";
4
4
  import { Primitive } from "reka-ui";
5
5
  //#endregion
@@ -17,6 +17,8 @@ var button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
17
17
  size: { default: "md" },
18
18
  intent: { default: "primary" },
19
19
  variant: { default: "fill" },
20
+ disabled: { type: Boolean },
21
+ loading: { type: Boolean },
20
22
  asChild: { type: Boolean },
21
23
  as: { default: "button" }
22
24
  },
@@ -26,17 +28,21 @@ var button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE_
26
28
  return openBlock(), createBlock(unref(Primitive), {
27
29
  as: __props.as,
28
30
  "as-child": __props.asChild,
29
- class: normalizeClass(unref(clsx$1)("celeste-button", `celeste-button-intent-${__props.intent}`, `celeste-button-size-${__props.size}`, `celeste-button-variant-${__props.variant}`, props.class))
31
+ disabled: __props.disabled || __props.loading || void 0,
32
+ "aria-busy": __props.loading || void 0,
33
+ class: normalizeClass(unref(clsx$1)("celeste-button", `celeste-button-intent-${__props.intent}`, `celeste-button-size-${__props.size}`, `celeste-button-variant-${__props.variant}`, __props.loading && !__props.asChild && "celeste-button-loading", props.class))
30
34
  }, {
31
- default: withCtx(() => [renderSlot(_ctx.$slots, "default", {}, void 0, true)]),
35
+ default: withCtx(() => [__props.asChild ? renderSlot(_ctx.$slots, "default", {}, void 0, true, 0) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [renderSlot(_ctx.$slots, "default", {}, void 0, true), _cache[0] || (_cache[0] = createElementVNode("i", { class: "celeste-button-spinner i-celeste-loader-5-line" }, null, -1))], 64))]),
32
36
  _: 3
33
37
  }, 8, [
34
38
  "as",
35
39
  "as-child",
40
+ "disabled",
41
+ "aria-busy",
36
42
  "class"
37
43
  ]);
38
44
  };
39
45
  }
40
- }), [["__scopeId", "data-v-0aeead06"]]);
46
+ }), [["__scopeId", "data-v-30b82d15"]]);
41
47
  //#endregion
42
48
  export { button_default as t };
@@ -38,6 +38,6 @@ var compact_button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/*
38
38
  ]);
39
39
  };
40
40
  }
41
- }), [["__scopeId", "data-v-bcc27daf"]]);
41
+ }), [["__scopeId", "data-v-0727facf"]]);
42
42
  //#endregion
43
43
  export { compact_button_default as t };
@@ -1,5 +1,5 @@
1
1
  import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper.js";
2
- import { createBlock, defineComponent, normalizeClass, openBlock, renderSlot, unref, withCtx } from "vue";
2
+ import { Fragment, createBlock, createElementBlock, createElementVNode, defineComponent, normalizeClass, openBlock, renderSlot, unref, withCtx } from "vue";
3
3
  import clsx$1 from "clsx";
4
4
  import { Primitive } from "reka-ui";
5
5
  //#endregion
@@ -16,6 +16,8 @@ var fancy_button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
16
16
  ] },
17
17
  size: { default: "md" },
18
18
  intent: { default: "primary" },
19
+ disabled: { type: Boolean },
20
+ loading: { type: Boolean },
19
21
  asChild: { type: Boolean },
20
22
  as: { default: "button" }
21
23
  },
@@ -25,17 +27,21 @@ var fancy_button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
25
27
  return openBlock(), createBlock(unref(Primitive), {
26
28
  as: __props.as,
27
29
  "as-child": __props.asChild,
28
- class: normalizeClass(unref(clsx$1)("celeste-fbutton", `celeste-fbutton-${__props.intent}`, `celeste-fbutton-${__props.size}`, props.class))
30
+ disabled: __props.disabled || __props.loading || void 0,
31
+ "aria-busy": __props.loading || void 0,
32
+ class: normalizeClass(unref(clsx$1)("celeste-fbutton", `celeste-fbutton-${__props.intent}`, `celeste-fbutton-${__props.size}`, __props.loading && !__props.asChild && "celeste-fbutton-loading", props.class))
29
33
  }, {
30
- default: withCtx(() => [renderSlot(_ctx.$slots, "default", {}, void 0, true)]),
34
+ default: withCtx(() => [__props.asChild ? renderSlot(_ctx.$slots, "default", {}, void 0, true, 0) : (openBlock(), createElementBlock(Fragment, { key: 1 }, [renderSlot(_ctx.$slots, "default", {}, void 0, true), _cache[0] || (_cache[0] = createElementVNode("i", { class: "celeste-fbutton-spinner i-celeste-loader-5-line" }, null, -1))], 64))]),
31
35
  _: 3
32
36
  }, 8, [
33
37
  "as",
34
38
  "as-child",
39
+ "disabled",
40
+ "aria-busy",
35
41
  "class"
36
42
  ]);
37
43
  };
38
44
  }
39
- }), [["__scopeId", "data-v-d53adf47"]]);
45
+ }), [["__scopeId", "data-v-9098298e"]]);
40
46
  //#endregion
41
47
  export { fancy_button_default as t };
@@ -31,6 +31,6 @@ var social_button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @
31
31
  }, 8, ["aria-label", "class"]);
32
32
  };
33
33
  }
34
- }), [["__scopeId", "data-v-3652f72b"]]);
34
+ }), [["__scopeId", "data-v-85544f28"]]);
35
35
  //#endregion
36
36
  export { social_button_default as t };
@@ -16,6 +16,8 @@ var text_input_button_default = /*#__PURE__*/ _plugin_vue_export_helper_default(
16
16
  Array
17
17
  ] },
18
18
  intent: {},
19
+ disabled: { type: Boolean },
20
+ loading: { type: Boolean },
19
21
  asChild: { type: Boolean },
20
22
  as: {},
21
23
  inline: {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@youcan/celeste",
3
3
  "type": "module",
4
- "version": "0.23.0",
4
+ "version": "0.24.0",
5
5
  "author": "YouCan <tech@youcan.shop>",
6
6
  "repository": {
7
7
  "type": "git",
@@ -52,8 +52,8 @@
52
52
  "reka-ui": "~2.10.5",
53
53
  "tinycolor2": "^1.6.0",
54
54
  "vue-sonner": "^2.0.9",
55
- "@youcan/celeste-tokens": "0.23.0",
56
- "@youcan/celeste-icons": "0.23.0"
55
+ "@youcan/celeste-icons": "0.24.0",
56
+ "@youcan/celeste-tokens": "0.24.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@chromatic-com/storybook": "^5.3.0",
@@ -9,6 +9,8 @@ export interface ButtonProps extends PrimitiveProps {
9
9
  size?: 'xxs' | 'xs' | 'sm' | 'md';
10
10
  intent?: 'primary' | 'neutral' | 'error';
11
11
  variant?: 'fill' | 'stroke' | 'lighter' | 'ghost';
12
+ disabled?: boolean;
13
+ loading?: boolean;
12
14
  }
13
15
 
14
16
  const props = withDefaults(defineProps<ButtonProps>(), {
@@ -23,15 +25,22 @@ const props = withDefaults(defineProps<ButtonProps>(), {
23
25
  <Primitive
24
26
  :as
25
27
  :as-child
28
+ :disabled="disabled || loading || undefined"
29
+ :aria-busy="loading || undefined"
26
30
  :class="clsx(
27
31
  'celeste-button',
28
32
  `celeste-button-intent-${intent}`,
29
33
  `celeste-button-size-${size}`,
30
34
  `celeste-button-variant-${variant}`,
35
+ loading && !asChild && 'celeste-button-loading',
31
36
  props.class,
32
37
  )"
33
38
  >
34
- <slot />
39
+ <slot v-if="asChild" />
40
+ <template v-else>
41
+ <slot />
42
+ <i class="celeste-button-spinner i-celeste-loader-5-line" />
43
+ </template>
35
44
  </Primitive>
36
45
  </template>
37
46
 
@@ -216,6 +225,7 @@ $hover-map: (
216
225
 
217
226
  .celeste-button {
218
227
  display: inline-flex;
228
+ position: relative;
219
229
  box-sizing: border-box;
220
230
  align-items: center;
221
231
  justify-content: center;
@@ -229,8 +239,9 @@ $hover-map: (
229
239
  text-decoration: none;
230
240
  white-space: nowrap;
231
241
  cursor: pointer;
242
+ user-select: none;
232
243
 
233
- &:disabled {
244
+ &:disabled:not(&-loading) {
234
245
  background-color: var(--color-bg-weak-50);
235
246
  color: var(--color-text-disabled-300);
236
247
  cursor: default;
@@ -249,7 +260,8 @@ $hover-map: (
249
260
  padding: map.get($v, padding);
250
261
  border-radius: map.get($v, border-radius);
251
262
 
252
- &:has(> :only-child:is(i)) {
263
+ &:has(> :only-child:is(i):not(.celeste-button-spinner)),
264
+ &:has(> i:first-child + .celeste-button-spinner:last-child) {
253
265
  padding: list.nth(map.get($v, padding), 1);
254
266
  }
255
267
  }
@@ -257,11 +269,17 @@ $hover-map: (
257
269
 
258
270
  @each $intent, $sub in $intent-map {
259
271
  @each $variant, $values in $sub {
260
- &-intent-#{$intent}:not(:disabled).celeste-button-variant-#{$variant}:not(:disabled) {
272
+ &-intent-#{$intent}.celeste-button-variant-#{$variant}:not(:disabled:not(.celeste-button-loading)) {
273
+ --celeste-button-fg: #{map.get($values, fg)};
274
+
261
275
  border-color: map.get($values, border);
262
276
  background-color: map.get($values, bg);
263
277
  box-shadow: map.get($values, shadow);
264
- color: map.get($values, fg);
278
+ color: var(--celeste-button-fg);
279
+
280
+ &.celeste-button-loading {
281
+ color: transparent;
282
+ }
265
283
 
266
284
  &:hover:not(:disabled) {
267
285
  $hover-values: map.get(map.get($hover-map, $intent), $variant);
@@ -283,4 +301,40 @@ $hover-map: (
283
301
  }
284
302
  }
285
303
  }
304
+
305
+ .celeste-button-loading {
306
+ pointer-events: none;
307
+ }
308
+
309
+ .celeste-button-spinner {
310
+ position: absolute;
311
+ margin: auto;
312
+ transition-property: opacity, scale;
313
+ transition-duration: var(--animation-fast);
314
+ transition-timing-function: ease-out;
315
+ animation: celeste-button-spin 600ms linear infinite paused;
316
+ opacity: 0;
317
+ color: var(--celeste-button-fg);
318
+ inset: 0;
319
+ scale: 0.9;
320
+ }
321
+
322
+ .celeste-button-loading > .celeste-button-spinner {
323
+ animation-play-state: running;
324
+ opacity: 1;
325
+ scale: 1;
326
+ }
327
+
328
+ @media (prefers-reduced-motion: reduce) {
329
+ .celeste-button-spinner {
330
+ animation-duration: 1200ms;
331
+ scale: 1;
332
+ }
333
+ }
334
+
335
+ @keyframes celeste-button-spin {
336
+ 100% {
337
+ rotate: 360deg;
338
+ }
339
+ }
286
340
  </style>
@@ -51,6 +51,7 @@ const props = withDefaults(defineProps<CompactButtonProps>(), {
51
51
  font: var(--label-sm);
52
52
  text-decoration: none;
53
53
  cursor: pointer;
54
+ user-select: none;
54
55
  gap: var(--spacing-2);
55
56
 
56
57
  &:disabled {
@@ -8,6 +8,8 @@ export interface FancyButtonProps extends PrimitiveProps {
8
8
  class?: HTMLAttributes['class'];
9
9
  size?: 'xs' | 'sm' | 'md';
10
10
  intent?: 'primary' | 'neutral' | 'error' | 'basic';
11
+ disabled?: boolean;
12
+ loading?: boolean;
11
13
  }
12
14
 
13
15
  const props = withDefaults(defineProps<FancyButtonProps>(), {
@@ -21,14 +23,21 @@ const props = withDefaults(defineProps<FancyButtonProps>(), {
21
23
  <Primitive
22
24
  :as
23
25
  :as-child
26
+ :disabled="disabled || loading || undefined"
27
+ :aria-busy="loading || undefined"
24
28
  :class="clsx(
25
29
  'celeste-fbutton',
26
30
  `celeste-fbutton-${intent}`,
27
31
  `celeste-fbutton-${size}`,
32
+ loading && !asChild && 'celeste-fbutton-loading',
28
33
  props.class,
29
34
  )"
30
35
  >
31
- <slot />
36
+ <slot v-if="asChild" />
37
+ <template v-else>
38
+ <slot />
39
+ <i class="celeste-fbutton-spinner i-celeste-loader-5-line" />
40
+ </template>
32
41
  </Primitive>
33
42
  </template>
34
43
 
@@ -95,8 +104,9 @@ $intent-map: (
95
104
  text-decoration: none;
96
105
  white-space: nowrap;
97
106
  cursor: pointer;
107
+ user-select: none;
98
108
 
99
- &:disabled {
109
+ &:disabled:not(&-loading) {
100
110
  background-color: var(--color-bg-weak-50);
101
111
  color: var(--color-text-disabled-300);
102
112
  cursor: default;
@@ -122,7 +132,8 @@ $intent-map: (
122
132
  }
123
133
 
124
134
  @each $k, $v in $intent-map {
125
- &-#{$k}:not(&-basic, :disabled) {
135
+ &-#{$k}:not(&-basic, :disabled:not(&-loading)) {
136
+ --celeste-fbutton-fg: #{if($k == 'neutral', var(--color-text-white-0), var(--color-static-white))};
126
137
  --celeste-fbutton-bg-start: rgb(255 255 255 / 16%);
127
138
  --celeste-fbutton-bg-end: rgb(255 255 255 / 0%);
128
139
 
@@ -130,7 +141,11 @@ $intent-map: (
130
141
  linear-gradient(180deg, var(--celeste-fbutton-bg-start) 0%, var(--celeste-fbutton-bg-end) 100%),
131
142
  #{map.get($v, bg)};
132
143
  box-shadow: var(--shadow-fancy-buttons-#{$k});
133
- color: if($k == 'neutral', var(--color-text-white-0), var(--color-static-white));
144
+ color: var(--celeste-fbutton-fg);
145
+
146
+ &.celeste-fbutton-loading {
147
+ color: transparent;
148
+ }
134
149
 
135
150
  &::before {
136
151
  content: '';
@@ -161,9 +176,15 @@ $intent-map: (
161
176
  }
162
177
 
163
178
  &-basic {
179
+ --celeste-fbutton-fg: var(--color-text-sub-600);
180
+
164
181
  background: var(--color-bg-white-0);
165
182
  box-shadow: var(--shadow-fancy-buttons-stroke);
166
- color: var(--color-text-sub-600);
183
+ color: var(--celeste-fbutton-fg);
184
+
185
+ &.celeste-fbutton-loading {
186
+ color: transparent;
187
+ }
167
188
 
168
189
  &:hover {
169
190
  background: var(--color-bg-weak-50);
@@ -178,4 +199,40 @@ $intent-map: (
178
199
  }
179
200
  }
180
201
  }
202
+
203
+ .celeste-fbutton-loading {
204
+ pointer-events: none;
205
+ }
206
+
207
+ .celeste-fbutton-spinner {
208
+ position: absolute;
209
+ margin: auto;
210
+ transition-property: opacity, scale;
211
+ transition-duration: var(--animation-fast);
212
+ transition-timing-function: ease-out;
213
+ animation: celeste-fbutton-spin 600ms linear infinite paused;
214
+ opacity: 0;
215
+ color: var(--celeste-fbutton-fg);
216
+ inset: 0;
217
+ scale: 0.9;
218
+ }
219
+
220
+ .celeste-fbutton-loading > .celeste-fbutton-spinner {
221
+ animation-play-state: running;
222
+ opacity: 1;
223
+ scale: 1;
224
+ }
225
+
226
+ @media (prefers-reduced-motion: reduce) {
227
+ .celeste-fbutton-spinner {
228
+ animation-duration: 1200ms;
229
+ scale: 1;
230
+ }
231
+ }
232
+
233
+ @keyframes celeste-fbutton-spin {
234
+ 100% {
235
+ rotate: 360deg;
236
+ }
237
+ }
181
238
  </style>
@@ -82,6 +82,7 @@ $brand-map: (
82
82
  text-decoration: none;
83
83
  white-space: nowrap;
84
84
  cursor: pointer;
85
+ user-select: none;
85
86
  gap: var(--spacing-8);
86
87
 
87
88
  span {
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
2
3
  import Button from '../button.vue';
3
4
 
4
5
  const meta: Meta<typeof Button> = {
@@ -49,3 +50,30 @@ export const IconOnly: Story = {
49
50
  `,
50
51
  }),
51
52
  };
53
+
54
+ export const Loading: Story = {
55
+ args: {
56
+ size: 'md',
57
+ intent: 'primary',
58
+ },
59
+
60
+ render: args => ({
61
+ components: { Button },
62
+ setup() {
63
+ const loading = ref(false);
64
+
65
+ function save(): void {
66
+ loading.value = true;
67
+ setTimeout(() => loading.value = false, 3000);
68
+ }
69
+
70
+ return { args, loading, save };
71
+ },
72
+ template: `
73
+ <Button v-bind="args" :loading="loading" @click="save">
74
+ Save changes
75
+ <i class="i-celeste-check-line"></i>
76
+ </Button>
77
+ `,
78
+ }),
79
+ };
@@ -1,4 +1,5 @@
1
1
  import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
2
3
  import FancyButton from '../fancy-button.vue';
3
4
 
4
5
  const meta: Meta<typeof FancyButton> = {
@@ -29,3 +30,30 @@ export const Default: Story = {
29
30
  `,
30
31
  }),
31
32
  };
33
+
34
+ export const Loading: Story = {
35
+ args: {
36
+ size: 'md',
37
+ intent: 'primary',
38
+ },
39
+
40
+ render: args => ({
41
+ components: { FancyButton },
42
+ setup() {
43
+ const loading = ref(false);
44
+
45
+ function save(): void {
46
+ loading.value = true;
47
+ setTimeout(() => loading.value = false, 3000);
48
+ }
49
+
50
+ return { args, loading, save };
51
+ },
52
+ template: `
53
+ <FancyButton v-bind="args" :loading="loading" @click="save">
54
+ Save changes
55
+ <i class="i-celeste-check-line"></i>
56
+ </FancyButton>
57
+ `,
58
+ }),
59
+ };