@youcan/celeste 0.17.2 → 0.19.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.
Files changed (66) hide show
  1. package/ai/README.md +1 -1
  2. package/ai/component-registry.json +206 -3
  3. package/ai/icons.json +2 -2
  4. package/ai/tokens.json +2 -2
  5. package/dist/celeste.css +740 -380
  6. package/dist/cjs/chunks/ComboboxItem.cjs +6 -0
  7. package/dist/cjs/chunks/alpha-slider.cjs +1 -1
  8. package/dist/cjs/chunks/color-area.cjs +1 -1
  9. package/dist/cjs/chunks/color-picker.cjs +20 -25
  10. package/dist/cjs/chunks/color-swatch.cjs +4 -3
  11. package/dist/cjs/chunks/combobox-item.cjs +63 -7
  12. package/dist/cjs/chunks/combobox-list.cjs +1 -1
  13. package/dist/cjs/chunks/combobox-trigger.cjs +1 -1
  14. package/dist/cjs/chunks/combobox.cjs +2 -2
  15. package/dist/cjs/chunks/components.cjs +7 -5
  16. package/dist/cjs/chunks/gradient-picker.cjs +635 -0
  17. package/dist/cjs/chunks/text-input-affix.cjs +1 -1
  18. package/dist/cjs/chunks/text-input-button.cjs +1 -1
  19. package/dist/cjs/chunks/text-input.cjs +2 -1
  20. package/dist/cjs/chunks/textarea.cjs +1 -1
  21. package/dist/{es/chunks/use-color-model.js → cjs/chunks/tinycolor.cjs} +6 -23
  22. package/dist/cjs/gradient-picker.cjs +2 -0
  23. package/dist/cjs/gradient.cjs +71 -0
  24. package/dist/cjs/index.cjs +10 -5
  25. package/dist/cjs/use-color-model.cjs +27 -3
  26. package/dist/components/color-picker/gradient-picker.d.ts +51 -0
  27. package/dist/components/color-picker/index.d.ts +2 -0
  28. package/dist/es/chunks/ComboboxItem.js +1 -1
  29. package/dist/es/chunks/alpha-slider.js +1 -1
  30. package/dist/es/chunks/color-area.js +1 -1
  31. package/dist/es/chunks/color-picker.js +12 -17
  32. package/dist/es/chunks/color-swatch.js +2 -1
  33. package/dist/es/chunks/combobox-item.js +65 -9
  34. package/dist/es/chunks/combobox-list.js +1 -1
  35. package/dist/es/chunks/combobox-trigger.js +1 -1
  36. package/dist/es/chunks/combobox.js +2 -2
  37. package/dist/es/chunks/components.js +7 -5
  38. package/dist/es/chunks/gradient-picker.js +629 -0
  39. package/dist/es/chunks/text-input-affix.js +1 -1
  40. package/dist/es/chunks/text-input-button.js +1 -1
  41. package/dist/es/chunks/text-input.js +2 -1
  42. package/dist/es/chunks/textarea.js +1 -1
  43. package/dist/{cjs/chunks/use-color-model.cjs → es/chunks/tinycolor.js} +1 -40
  44. package/dist/es/gradient-picker.js +2 -0
  45. package/dist/es/gradient.js +69 -0
  46. package/dist/es/index.js +8 -6
  47. package/dist/es/use-color-model.js +25 -1
  48. package/dist/utils/gradient.d.ts +13 -0
  49. package/dist/utils/index.d.ts +1 -0
  50. package/package.json +3 -3
  51. package/src/components/color-picker/color-picker.vue +8 -9
  52. package/src/components/color-picker/composable/use-color-model.ts +5 -0
  53. package/src/components/color-picker/gradient-picker.vue +944 -0
  54. package/src/components/color-picker/index.ts +3 -0
  55. package/src/components/color-picker/stories/gradient-picker.stories.ts +37 -0
  56. package/src/components/combobox/combobox-item.vue +15 -7
  57. package/src/components/combobox/combobox-list.vue +2 -2
  58. package/src/components/combobox/combobox-trigger.vue +38 -0
  59. package/src/components/combobox/combobox.vue +2 -0
  60. package/src/components/text-input/stories/text-input.stories.ts +53 -3
  61. package/src/components/text-input/text-input-affix.vue +7 -3
  62. package/src/components/text-input/text-input-button.vue +6 -2
  63. package/src/components/text-input/text-input.vue +45 -34
  64. package/src/components/textarea/textarea.vue +1 -1
  65. package/src/utils/gradient.ts +90 -0
  66. package/src/utils/index.ts +1 -0
@@ -10,5 +10,8 @@ export { default as ColorPicker } from './color-picker.vue';
10
10
  export * from './color-swatch.vue';
11
11
  export { default as ColorSwatch } from './color-swatch.vue';
12
12
 
13
+ export * from './gradient-picker.vue';
14
+ export { default as GradientPicker } from './gradient-picker.vue';
15
+
13
16
  export * from './hue-slider.vue';
14
17
  export { default as HueSlider } from './hue-slider.vue';
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3-vite';
2
+ import { ref } from 'vue';
3
+ import GradientPicker from '../gradient-picker.vue';
4
+
5
+ const meta: Meta<typeof GradientPicker> = {
6
+ title: 'Components/Gradient Picker',
7
+ component: GradientPicker,
8
+ parameters: {
9
+ layout: 'padded',
10
+ },
11
+ };
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof GradientPicker>;
16
+
17
+ export const Default: Story = {
18
+ render: () => ({
19
+ components: { GradientPicker },
20
+ setup() {
21
+ const gradient = ref('linear-gradient(135deg, #3B82F6 0%, #EC4899 100%)');
22
+ return { gradient };
23
+ },
24
+ template: `<div style="display: grid; width: 280px; gap: 8px;"><GradientPicker v-model="gradient" /><code>{{ gradient }}</code></div>`,
25
+ }),
26
+ };
27
+
28
+ export const Empty: Story = {
29
+ render: () => ({
30
+ components: { GradientPicker },
31
+ setup() {
32
+ const gradient = ref('');
33
+ return { gradient };
34
+ },
35
+ template: `<div style="display: grid; width: 280px; gap: 8px;"><GradientPicker v-model="gradient" /><code>{{ gradient }}</code></div>`,
36
+ }),
37
+ };
@@ -2,7 +2,7 @@
2
2
  import type { ComboboxItemEmits } from 'reka-ui';
3
3
  import type { HTMLAttributes } from 'vue';
4
4
  import clsx from 'clsx';
5
- import { ComboboxItem, useForwardPropsEmits } from 'reka-ui';
5
+ import { ComboboxItem, ComboboxItemIndicator, useForwardPropsEmits } from 'reka-ui';
6
6
  import { computed } from 'vue';
7
7
  import Checkbox from '../checkbox/checkbox.vue';
8
8
 
@@ -48,7 +48,6 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
48
48
  props.class,
49
49
  'celeste-dropdown-item',
50
50
  { 'celeste-dropdown-item-disabled': disabled },
51
- { 'celeste-dropdown-item-selected': selected },
52
51
  { 'celeste-dropdown-item-focused': focused },
53
52
  )"
54
53
  >
@@ -83,7 +82,9 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
83
82
  <div v-if="$slots.suffix" class="celeste-dropdown-item-suffix">
84
83
  <slot name="suffix" />
85
84
  </div>
86
- <i v-if="selected" class="i-celeste-check-fill" />
85
+ <ComboboxItemIndicator as-child>
86
+ <i class="celeste-dropdown-item-indicator i-celeste-check-line" />
87
+ </ComboboxItemIndicator>
87
88
  </div>
88
89
  </ComboboxItem>
89
90
  </template>
@@ -91,8 +92,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
91
92
  <style lang="scss" scoped>
92
93
  .celeste-dropdown-item {
93
94
  display: flex;
95
+ position: relative;
94
96
  align-items: center;
95
97
  padding: var(--spacing-8);
98
+ padding-inline-end: var(--spacing-36);
96
99
  transition-property: background-color;
97
100
  transition-duration: var(--animation-fast);
98
101
  transition-timing-function: ease-out;
@@ -159,6 +162,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
159
162
  color: var(--color-icon-sub-600);
160
163
  }
161
164
 
165
+ &-indicator {
166
+ position: absolute;
167
+ top: 50%;
168
+ width: 20px;
169
+ height: 20px;
170
+ transform: translateY(-50%);
171
+ inset-inline-end: var(--spacing-8);
172
+ }
173
+
162
174
  &-disabled {
163
175
  color: var(--color-text-disabled-300);
164
176
  cursor: not-allowed;
@@ -174,10 +186,6 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
174
186
  }
175
187
  }
176
188
 
177
- &-selected {
178
- background: var(--color-bg-weak-50);
179
- }
180
-
181
189
  .celeste-dropdown-item-info {
182
190
  display: flex;
183
191
  flex: 1 0 0;
@@ -39,14 +39,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits);
39
39
  div:deep(.celeste-dropdown-content) {
40
40
  --dropdown-height: 300px;
41
41
  --dropdown-min-width: var(--reka-combobox-trigger-width, 300px);
42
- --dropdown-width: max(var(--reka-popper-anchor-width), 300px);
43
42
 
44
43
  display: flex;
45
44
  z-index: 50;
46
45
  box-sizing: border-box;
47
46
  flex-direction: column;
48
- width: var(--dropdown-width);
47
+ width: max-content;
49
48
  min-width: var(--dropdown-min-width);
49
+ max-width: max(var(--reka-popper-anchor-width), 320px);
50
50
  max-height: var(--dropdown-height);
51
51
  margin-top: var(--spacing-10);
52
52
  padding: var(--spacing-8);
@@ -197,6 +197,19 @@ const forwarded = useForwardProps(delegatedProps);
197
197
  border: none;
198
198
  border-radius: 0;
199
199
  box-shadow: none;
200
+ gap: 0;
201
+
202
+ :deep(.celeste-dropdown-anchor-trigger-prefix) {
203
+ margin-inline-end: var(--spacing-8);
204
+ }
205
+
206
+ :deep(.celeste-dropdown-trigger) {
207
+ margin-inline-start: var(--spacing-2);
208
+ }
209
+
210
+ &.celeste-dropdown-anchor-trigger-filled:not([data-disabled]) :deep(.celeste-dropdown-trigger i) {
211
+ color: var(--color-icon-sub-600);
212
+ }
200
213
 
201
214
  &:deep(.celeste-dropdown-trigger-label) {
202
215
  flex: inherit;
@@ -206,6 +219,31 @@ const forwarded = useForwardProps(delegatedProps);
206
219
  &[data-state='open'] {
207
220
  box-shadow: none;
208
221
  }
222
+
223
+ &:focus-visible {
224
+ background: var(--color-bg-weak-50);
225
+ }
226
+
227
+ &.celeste-dropdown-anchor-trigger-size-md {
228
+ padding-inline: var(--spacing-10) var(--spacing-8);
229
+ }
230
+
231
+ &.celeste-dropdown-anchor-trigger-size-sm {
232
+ padding-inline: var(--spacing-8);
233
+ }
234
+
235
+ &.celeste-dropdown-anchor-trigger-size-xs {
236
+ padding-inline: var(--spacing-8) var(--spacing-6);
237
+
238
+ :deep(.celeste-dropdown-anchor-trigger-prefix) {
239
+ margin-inline-end: var(--spacing-6);
240
+ }
241
+
242
+ :deep(.celeste-dropdown-anchor-trigger-prefix :is(i, .selected-dropdown-image)) {
243
+ width: 16px;
244
+ height: 16px;
245
+ }
246
+ }
209
247
  }
210
248
 
211
249
  &.celeste-dropdown-anchor-trigger-type-inline {
@@ -253,6 +253,7 @@ const filteredOptions = computed(() => props.ignoreFilter
253
253
  align-items: center;
254
254
  justify-content: center;
255
255
  width: 20px;
256
+ max-width: none;
256
257
  height: 20px;
257
258
  object-fit: cover;
258
259
  border-radius: var(--radius-full);
@@ -274,6 +275,7 @@ const filteredOptions = computed(() => props.ignoreFilter
274
275
  overflow: hidden;
275
276
  text-overflow: ellipsis;
276
277
  white-space: nowrap;
278
+ unicode-bidi: plaintext;
277
279
  }
278
280
  }
279
281
 
@@ -73,15 +73,21 @@ export const Size: Story = {
73
73
  template: `
74
74
  <div style="display: grid; gap: 20px; justify-content: center;">
75
75
  <TextInput v-bind="args" style="width: 300px" size="md">
76
- <i class="i-celeste-user-6-line" />
76
+ <template #leadingIcon>
77
+ <i class="i-celeste-user-6-line" />
78
+ </template>
77
79
  </TextInput>
78
80
 
79
81
  <TextInput v-bind="args" style="width: 300px" size="sm">
80
- <i class="i-celeste-user-6-line" />
82
+ <template #leadingIcon>
83
+ <i class="i-celeste-user-6-line" />
84
+ </template>
81
85
  </TextInput>
82
86
 
83
87
  <TextInput v-bind="args" style="width: 300px" size="xs">
84
- <i class="i-celeste-user-6-line" />
88
+ <template #leadingIcon>
89
+ <i class="i-celeste-user-6-line" />
90
+ </template>
85
91
  </TextInput>
86
92
  </div>
87
93
  `,
@@ -361,3 +367,47 @@ export const WithTrailingCombobox: Story = {
361
367
  `,
362
368
  }),
363
369
  };
370
+
371
+ function flag(color: string): string {
372
+ return `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><circle cx="10" cy="10" r="10" fill="${color}"/></svg>`)}`;
373
+ }
374
+
375
+ const COUNTRIES = [
376
+ { value: 'MA', label: 'Morocco (+212)', shortLabel: '+212', image: flag('#c1272d') },
377
+ { value: 'FR', label: 'France (+33)', shortLabel: '+33', image: flag('#002654') },
378
+ { value: 'US', label: 'United States (+1)', shortLabel: '+1', image: flag('#3c3b6e') },
379
+ ];
380
+
381
+ export const Phone: Story = {
382
+ args: {
383
+ type: 'tel',
384
+ placeholder: '610-101010',
385
+ },
386
+
387
+ render: args => ({
388
+ components: { TextInput, Combobox },
389
+ setup() {
390
+ const country = ref(COUNTRIES[0]);
391
+ const phone = ref('+212610101010');
392
+
393
+ return { args, COUNTRIES, country, phone };
394
+ },
395
+ template: `
396
+ <div style="display: grid; gap: 20px; width: 360px;">
397
+ <TextInput v-for="size in ['md', 'sm', 'xs']" :key="size" v-bind="args" v-model="phone" :size="size">
398
+ <template #leadingNode>
399
+ <Combobox
400
+ v-model="country"
401
+ searchable
402
+ :options="COUNTRIES"
403
+ :size="size"
404
+ value-by="value"
405
+ label-by="shortLabel"
406
+ type="compact-input"
407
+ />
408
+ </template>
409
+ </TextInput>
410
+ </div>
411
+ `,
412
+ }),
413
+ };
@@ -27,18 +27,22 @@ const delegatedProps = useDelegatedProps(props, 'class');
27
27
  flex-shrink: 0;
28
28
  align-items: center;
29
29
  color: var(--celeste-text-input-affix-color);
30
+ unicode-bidi: plaintext;
30
31
 
31
32
  &[inline='false'] {
32
- padding-inline: var(--spacing-10);
33
+ padding-inline: var(--input-affix-padding);
33
34
  background-color: var(--celeste-text-input-affix-background-color);
34
35
  }
35
36
 
36
37
  &[askbd='true'] {
38
+ box-sizing: border-box;
37
39
  width: fit-content;
38
- height: fit-content;
39
- padding: var(--spacing-2) calc(var(--spacing-8) - var(--spacing-2));
40
+ height: 20px;
41
+ padding-inline: var(--spacing-6);
40
42
  border: 1px solid var(--color-stroke-soft-200);
41
43
  border-radius: var(--radius-4);
44
+ background-color: var(--color-bg-white-0);
45
+ color: var(--celeste-text-input-muted-color);
42
46
  font: var(--subheading-xs);
43
47
  }
44
48
  }
@@ -37,10 +37,14 @@ const delegatedProps = useDelegatedProps(props, 'class');
37
37
  }
38
38
 
39
39
  &[inline='false'] {
40
- padding: var(--spacing-10);
40
+ padding-inline: var(--spacing-14);
41
41
  border-end-start-radius: 0;
42
42
  border-start-start-radius: 0;
43
43
 
44
+ &:has(> :only-child:is(i)) {
45
+ padding-inline: var(--spacing-10);
46
+ }
47
+
44
48
  &:is(.celeste-button-intent-neutral, .celeste-button-variant-stroke):not(:disabled) {
45
49
  background-color: var(--color-bg-white-0);
46
50
 
@@ -53,7 +57,7 @@ const delegatedProps = useDelegatedProps(props, 'class');
53
57
  &[inline='true'] {
54
58
  width: 20px;
55
59
  height: 20px;
56
- color: var(--celeste-text-input-icon-color) !important;
60
+ color: var(--celeste-text-input-muted-color) !important;
57
61
 
58
62
  &:hover {
59
63
  background-color: transparent !important;
@@ -80,6 +80,7 @@ function clearInput() {
80
80
  >
81
81
  <TextInputButton
82
82
  v-if="showClearButton"
83
+ inline
83
84
  type="button"
84
85
  aria-label="Clear search"
85
86
  @click="clearInput"
@@ -97,8 +98,9 @@ function clearInput() {
97
98
  .celeste-text-input-wrapper {
98
99
  --celeste-text-input-border-color: var(--color-stroke-soft-200);
99
100
  --celeste-text-input-drop-shadow: var(--shadow-regular-xs);
100
- --celeste-text-input-icon-color: var(--color-text-soft-400);
101
- --celeste-text-input-affix-color: var(--color-text-soft-400);
101
+ --celeste-text-input-icon-color: var(--color-text-sub-600);
102
+ --celeste-text-input-affix-color: var(--color-text-sub-600);
103
+ --celeste-text-input-muted-color: var(--color-text-soft-400);
102
104
  --celeste-text-input-placeholder-color: var(--color-text-soft-400);
103
105
  --celeste-text-input-affix-background-color: var(--color-bg-white-0);
104
106
 
@@ -106,12 +108,10 @@ function clearInput() {
106
108
  box-sizing: border-box;
107
109
  align-items: center;
108
110
  height: var(--input-height);
109
- padding-inline: calc(var(--input-padding) + var(--spacing-2)) var(--input-padding);
111
+ padding-inline: var(--input-padding);
110
112
  transition: all var(--animation-fast) ease-out;
111
113
  border: 1px solid var(--celeste-text-input-border-color);
112
114
  border-radius: var(--input-radius);
113
- outline: 2px solid transparent;
114
- outline-offset: 2px;
115
115
  background-color: var(--color-bg-white-0);
116
116
  box-shadow: var(--celeste-text-input-drop-shadow);
117
117
  color: var(--color-text-strong-950);
@@ -119,24 +119,27 @@ function clearInput() {
119
119
  gap: var(--input-gap);
120
120
 
121
121
  &[size='xs'] {
122
- --input-gap: 8px;
122
+ --input-gap: var(--spacing-6);
123
123
  --input-height: 32px;
124
124
  --input-radius: var(--radius-8);
125
- --input-padding: calc(var(--spacing-8) - var(--spacing-2));
125
+ --input-padding: var(--spacing-8);
126
+ --input-affix-padding: var(--spacing-10);
126
127
  }
127
128
 
128
129
  &[size='sm'] {
129
- --input-gap: 6px;
130
+ --input-gap: var(--spacing-8);
130
131
  --input-height: 36px;
131
132
  --input-radius: var(--radius-8);
132
- --input-padding: var(--spacing-8);
133
+ --input-padding: var(--spacing-10);
134
+ --input-affix-padding: var(--spacing-10);
133
135
  }
134
136
 
135
137
  &[size='md'] {
136
- --input-gap: 6px;
138
+ --input-gap: var(--spacing-8);
137
139
  --input-height: 40px;
138
140
  --input-radius: var(--radius-10);
139
- --input-padding: var(--spacing-10);
141
+ --input-padding: var(--spacing-12);
142
+ --input-affix-padding: var(--spacing-12);
140
143
  }
141
144
 
142
145
  &[has-error='true'] {
@@ -158,6 +161,12 @@ function clearInput() {
158
161
  color: var(--celeste-text-input-placeholder-color);
159
162
  }
160
163
 
164
+ &[type='tel'] {
165
+ direction: ltr;
166
+ text-align: -webkit-match-parent;
167
+ text-align: match-parent;
168
+ }
169
+
161
170
  // Hide default webkit search cancel button
162
171
  &[type='search'] {
163
172
  &::-webkit-search-cancel-button {
@@ -199,18 +208,36 @@ function clearInput() {
199
208
  padding-inline-end: 0;
200
209
  }
201
210
 
211
+ &:has(.celeste-text-input:placeholder-shown) {
212
+ --celeste-text-input-icon-color: var(--color-text-soft-400);
213
+ --celeste-text-input-affix-color: var(--color-text-soft-400);
214
+ }
215
+
216
+ &:hover:not(:has(.celeste-text-input:focus)) {
217
+ --celeste-text-input-drop-shadow: none;
218
+
219
+ &:not([has-error='true'], :has(.celeste-text-input-node)) {
220
+ --celeste-text-input-border-color: transparent;
221
+ }
222
+
223
+ &:not(:has(.celeste-text-input-node:hover)) {
224
+ --celeste-text-input-icon-color: var(--color-text-sub-600);
225
+ --celeste-text-input-placeholder-color: var(--color-text-sub-600);
226
+
227
+ background-color: var(--color-bg-weak-50);
228
+ }
229
+ }
230
+
202
231
  &:has(.celeste-text-input:focus) {
203
232
  --celeste-text-input-border-color: var(--color-stroke-strong-950);
233
+ --celeste-text-input-drop-shadow: var(--shadow-buttons-important-focus);
204
234
  --celeste-text-input-placeholder-color: var(--color-text-sub-600);
205
235
  --celeste-text-input-icon-color: var(--color-text-sub-600);
206
236
  --celeste-text-input-affix-color: var(--color-text-sub-600);
207
237
 
208
- outline-color: var(--color-stroke-soft-200);
209
-
210
238
  &[has-error='true'] {
211
239
  --celeste-text-input-border-color: var(--color-state-error-base);
212
-
213
- outline-color: var(--color-state-error-lighter);
240
+ --celeste-text-input-drop-shadow: var(--shadow-buttons-error-focus);
214
241
  }
215
242
  }
216
243
 
@@ -229,35 +256,17 @@ function clearInput() {
229
256
  --celeste-text-input-drop-shadow: none;
230
257
  --celeste-text-input-affix-background-color: var(--color-bg-weak-50);
231
258
  --celeste-text-input-affix-color: var(--color-text-disabled-300);
259
+ --celeste-text-input-muted-color: var(--color-text-disabled-300);
232
260
  --celeste-text-input-placeholder-color: var(--color-text-disabled-300);
233
261
  --celeste-text-input-icon-color: var(--color-text-disabled-300);
234
262
 
235
263
  background-color: var(--color-bg-weak-50);
236
- box-shadow: none;
237
264
  color: var(--color-text-disabled-300);
238
265
  pointer-events: none;
239
266
 
240
267
  .celeste-text-input::placeholder {
241
268
  color: currentcolor;
242
269
  }
243
-
244
- .celeste-text-input-clear-button {
245
- color: var(--color-text-disabled-300);
246
- pointer-events: none;
247
- }
248
- }
249
-
250
- &:has(.celeste-text-input:hover:not(:focus, :disabled)) {
251
- &:not(:has(.celeste-text-input-node)) {
252
- --celeste-text-input-border-color: transparent;
253
- }
254
-
255
- &:not(:has(.celeste-text-input-node:hover)) {
256
- --celeste-text-input-icon-color: var(--color-text-sub-600);
257
- --celeste-text-input-placeholder-color: var(--color-text-sub-600);
258
-
259
- background-color: var(--color-bg-weak-50);
260
- }
261
270
  }
262
271
 
263
272
  .celeste-text-input-icon {
@@ -274,6 +283,7 @@ function clearInput() {
274
283
  }
275
284
 
276
285
  :deep(.celeste-text-input-node.celeste-text-input-leading-node) {
286
+ margin-inline-end: calc(var(--input-padding) - var(--input-gap));
277
287
  border-inline-end: 1px solid var(--color-stroke-soft-200);
278
288
  border-start-start-radius: var(--input-radius);
279
289
  border-start-end-radius: 0;
@@ -282,6 +292,7 @@ function clearInput() {
282
292
  }
283
293
 
284
294
  :deep(.celeste-text-input-node.celeste-text-input-trailing-node) {
295
+ margin-inline-start: calc(var(--input-padding) - var(--input-gap));
285
296
  border-inline-start: 1px solid var(--color-stroke-soft-200);
286
297
  border-start-start-radius: 0;
287
298
  border-start-end-radius: var(--input-radius);
@@ -88,7 +88,7 @@ const modelValue = defineModel<string>();
88
88
  display: flex;
89
89
  position: absolute;
90
90
  color: var(--color-text-soft-400);
91
- font: var(--subheading-2xs);
91
+ font: var(--subheading-xxs);
92
92
  inset-block-end: var(--spacing);
93
93
  inset-inline-end: calc(var(--spacing) + var(--resize-handle-offset));
94
94
  }
@@ -0,0 +1,90 @@
1
+ export type GradientType = 'linear' | 'radial';
2
+
3
+ export interface GradientStop {
4
+ color: string;
5
+ position: number;
6
+ }
7
+
8
+ export interface Gradient {
9
+ type: GradientType;
10
+ angle: number;
11
+ shape?: string;
12
+ stops: GradientStop[];
13
+ }
14
+
15
+ const DIRECTIONS: Record<string, number> = {
16
+ 'to top': 0,
17
+ 'to top right': 45,
18
+ 'to right top': 45,
19
+ 'to right': 90,
20
+ 'to bottom right': 135,
21
+ 'to right bottom': 135,
22
+ 'to bottom': 180,
23
+ 'to bottom left': 225,
24
+ 'to left bottom': 225,
25
+ 'to left': 270,
26
+ 'to top left': 315,
27
+ 'to left top': 315,
28
+ };
29
+
30
+ const UNITS: Record<string, number> = { deg: 1, grad: 0.9, rad: 180 / Math.PI, turn: 360 };
31
+
32
+ function splitTopLevel(value: string): string[] {
33
+ const parts: string[] = [];
34
+ let depth = 0;
35
+ let current = '';
36
+
37
+ for (const char of value) {
38
+ depth += char === '(' ? 1 : char === ')' ? -1 : 0;
39
+
40
+ if (char === ',' && depth === 0) {
41
+ parts.push(current.trim());
42
+ current = '';
43
+ continue;
44
+ }
45
+
46
+ current += char;
47
+ }
48
+
49
+ return [...parts, current.trim()].filter(Boolean);
50
+ }
51
+
52
+ export function parseGradient(value?: string | null): Gradient | null {
53
+ const match = /^\s*(linear|radial)-gradient\(([\s\S]*)\)\s*$/i.exec(value ?? '');
54
+
55
+ if (!match) {
56
+ return null;
57
+ }
58
+
59
+ const parts = splitTopLevel(match[2]);
60
+ const head = parts[0]?.toLowerCase() ?? '';
61
+ let angle = 180;
62
+
63
+ const unit = /^(-?(?:\d+(?:\.\d+)?|\.\d+))(deg|grad|rad|turn)$/.exec(head);
64
+
65
+ const shape = /circle|ellipse|closest|farthest|^at /.test(head) ? parts[0] : undefined;
66
+
67
+ if (unit || shape || head.startsWith('to ')) {
68
+ parts.shift();
69
+ angle = unit ? Math.round(((Number(unit[1]) * UNITS[unit[2]]) % 360 + 360) % 360) : DIRECTIONS[head] ?? angle;
70
+ }
71
+
72
+ const stops = parts.map((part, index) => {
73
+ const position = /\s(-?\d+(?:\.\d+)?)%$/.exec(part)?.[1];
74
+
75
+ return {
76
+ color: (position === undefined ? part : part.slice(0, part.lastIndexOf(' '))).trim(),
77
+ position: position === undefined ? Math.round((index / Math.max(parts.length - 1, 1)) * 100) : Number(position),
78
+ };
79
+ });
80
+
81
+ const type = match[1].toLowerCase() as GradientType;
82
+
83
+ return stops.length > 1 ? { type, angle, ...(type === 'radial' && shape && { shape }), stops } : null;
84
+ }
85
+
86
+ export function stringifyGradient({ type, angle, shape, stops }: Gradient): string {
87
+ const list = [...stops].sort((a, b) => a.position - b.position).map(stop => `${stop.color} ${stop.position}%`).join(', ');
88
+
89
+ return type === 'linear' ? `linear-gradient(${angle}deg, ${list})` : `radial-gradient(${shape ? `${shape}, ` : ''}${list})`;
90
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './chart';
2
2
  export * from './color';
3
3
  export * from './crypto';
4
+ export * from './gradient';
4
5
  export * from './sonner';