@refraction-ui/astro 0.16.1 → 0.17.1

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.
@@ -0,0 +1,56 @@
1
+ ---
2
+ import { createLocationSelector } from '../location-selector/index.ts'
3
+ import { cn } from '../shared/index.ts'
4
+
5
+ export interface Props {
6
+ defaultCountry?: string
7
+ defaultLanguage?: string
8
+ className?: string
9
+ }
10
+
11
+ const {
12
+ defaultCountry = 'US',
13
+ defaultLanguage = 'en',
14
+ className
15
+ } = Astro.props
16
+
17
+ const api = createLocationSelector({
18
+ defaultCountry,
19
+ defaultLanguage
20
+ })
21
+ ---
22
+
23
+ <div class={cn('flex flex-col gap-4 sm:flex-row', className)}>
24
+ <div class="flex flex-col gap-1.5 flex-1">
25
+ <label for={api.countryProps.id} class="text-sm font-medium">Country</label>
26
+ <select
27
+ id={api.countryProps.id}
28
+ name={api.countryProps.name}
29
+ class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
30
+ >
31
+ {api.countries.map((c: any) => {
32
+ const value = c.code || c.id || c;
33
+ const label = c.name || c.label || c;
34
+ return (
35
+ <option value={value} selected={value === defaultCountry}>{label}</option>
36
+ )
37
+ })}
38
+ </select>
39
+ </div>
40
+ <div class="flex flex-col gap-1.5 flex-1">
41
+ <label for={api.languageProps.id} class="text-sm font-medium">Language</label>
42
+ <select
43
+ id={api.languageProps.id}
44
+ name={api.languageProps.name}
45
+ class="flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
46
+ >
47
+ {api.languages.map((l: any) => {
48
+ const value = l.code || l.id || l;
49
+ const label = l.name || l.label || l;
50
+ return (
51
+ <option value={value} selected={value === defaultLanguage}>{label}</option>
52
+ )
53
+ })}
54
+ </select>
55
+ </div>
56
+ </div>
@@ -0,0 +1,15 @@
1
+ import Component from './LocationSelector.astro'
2
+
3
+ const meta = {
4
+ title: 'Astro/LocationSelector',
5
+ component: Component,
6
+ }
7
+
8
+ export default meta
9
+
10
+ export const Default = {
11
+ args: {
12
+ defaultCountry: 'Example defaultCountry',
13
+ defaultLanguage: 'Example defaultLanguage'
14
+ }
15
+ }
@@ -0,0 +1 @@
1
+ export { default as LocationSelector } from './LocationSelector.astro'
@@ -97,10 +97,10 @@ export class CommandInput {
97
97
  const matchedTrigger = this.options.triggers.find((t) => t.char === char);
98
98
 
99
99
  if (matchedTrigger) {
100
- const isStartOfWord = i === 0 || /\\s/.test(rawText[i - 1]);
100
+ const isStartOfWord = i === 0 || /\s/.test(rawText[i - 1]);
101
101
  if (isStartOfWord) {
102
102
  const commandText = rawText.slice(i + 1, cursorPosition);
103
- if (!/\\s/.test(commandText)) {
103
+ if (!/\s/.test(commandText)) {
104
104
  let isValid = true;
105
105
  if (matchedTrigger.pattern) {
106
106
  isValid = matchedTrigger.pattern.test(commandText);
@@ -118,7 +118,7 @@ export class CommandInput {
118
118
  break;
119
119
  }
120
120
 
121
- if (/\\s/.test(char)) {
121
+ if (/\s/.test(char)) {
122
122
  break;
123
123
  }
124
124
  }
@@ -130,8 +130,8 @@ export class CommandInput {
130
130
  return;
131
131
  }
132
132
 
133
- const triggerChars = this.options.triggers.map((t) => t.char.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'));
134
- const patternStr = `(^|\\\\s)(${triggerChars.join('|')})([^\\\\s]*)`;
133
+ const triggerChars = this.options.triggers.map((t) => t.char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
134
+ const patternStr = `(^|\\s)(${triggerChars.join('|')})([^\\s]*)`;
135
135
  const regex = new RegExp(patternStr, 'g');
136
136
 
137
137
  let currentIndex = 0;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `createEmojiTrigger` — a ready-made `:` shortcode trigger backed by the shared
3
+ * emoji dataset (`@refraction-ui/emoji-picker`, ~1900 base emoji), so the
4
+ * composer's `:` menu is complete and matches the picker exactly (one source of
5
+ * truth, no divergence). The committed token is the native glyph.
6
+ *
7
+ * This module is side-effect free and imports the dataset lazily via the emoji
8
+ * search API, so it is only pulled into a bundle when `createEmojiTrigger` is
9
+ * actually referenced.
10
+ */
11
+ import { searchEmojis } from '../emoji-picker/index.ts'
12
+ import type { ComposerCandidate, ComposerTrigger } from './types.js'
13
+
14
+ export interface EmojiTriggerOptions {
15
+ /** Trigger id. Default 'emoji'. */
16
+ id?: string
17
+ /** Symbol. Default ':'. */
18
+ symbol?: string
19
+ /** Max results surfaced to the menu. Default 8. */
20
+ maxResults?: number
21
+ /**
22
+ * Minimum query length before resolving (avoids flooding the menu with the
23
+ * whole set on a lone ':'). Default 1.
24
+ */
25
+ minQueryLength?: number
26
+ }
27
+
28
+ /**
29
+ * Build a `:` emoji trigger. Typing `:fir` filters the full set; committing
30
+ * inserts the glyph (🔥). Pair it with the picker for the same coverage in both
31
+ * surfaces.
32
+ */
33
+ export function createEmojiTrigger(options: EmojiTriggerOptions = {}): ComposerTrigger {
34
+ const { id = 'emoji', symbol = ':', maxResults = 8, minQueryLength = 1 } = options
35
+ return {
36
+ id,
37
+ symbol,
38
+ // The committed token is the glyph itself, not `:name:`.
39
+ toDisplay: (candidate: ComposerCandidate) =>
40
+ (candidate.metadata?.emoji as string | undefined) ?? candidate.display,
41
+ resolve: (query: string): ComposerCandidate[] => {
42
+ if (query.length < minQueryLength) return []
43
+ return searchEmojis(query, maxResults).map((entry) => ({
44
+ id: `${entry.shortcode}`,
45
+ display: `${entry.emoji} :${entry.shortcode}:`,
46
+ subtitle: entry.name,
47
+ metadata: { emoji: entry.emoji, shortcode: entry.shortcode },
48
+ }))
49
+ },
50
+ }
51
+ }
@@ -6,3 +6,4 @@ export * from './suggestion.ts'
6
6
  export * from './tokens.ts'
7
7
  export * from './composer.ts'
8
8
  export * from './styles.ts'
9
+ export { createEmojiTrigger, type EmojiTriggerOptions } from './emoji-trigger.js'
@@ -6,13 +6,25 @@
6
6
 
7
7
  import { cva } from '../shared/index.ts'
8
8
 
9
- /** Outer surface (the rounded card wrapping tray + field + action row). */
9
+ /**
10
+ * Outer surface (the rounded card wrapping tray + field + action row).
11
+ * `surface` picks the resting fill: `outlined` (transparent-on-page card) or
12
+ * `filled` (a calm muted fill distinct from the page). Both keep a tasteful
13
+ * focus-visible ring on the pill — a hairline border plus a single-px ring, not
14
+ * a slammed saturated full-perimeter brand border.
15
+ */
10
16
  export const composerSurfaceVariants = cva({
11
17
  base: [
12
- 'overflow-hidden rounded-2xl border border-border bg-background shadow-sm',
13
- 'transition-shadow focus-within:border-ring focus-within:ring-1 focus-within:ring-ring focus-within:shadow-md',
18
+ 'overflow-hidden rounded-2xl border shadow-sm',
19
+ 'transition-[box-shadow,border-color,background-color] duration-150 ease-out',
20
+ 'focus-within:ring-1 focus-within:ring-ring focus-within:shadow-md',
21
+ 'motion-reduce:transition-none',
14
22
  ].join(' '),
15
23
  variants: {
24
+ surface: {
25
+ outlined: 'border-border bg-background focus-within:border-ring',
26
+ filled: 'border-border/50 bg-muted/50 focus-within:border-ring/70 focus-within:bg-background',
27
+ },
16
28
  disabled: {
17
29
  true: 'pointer-events-none opacity-60',
18
30
  false: '',
@@ -23,6 +35,7 @@ export const composerSurfaceVariants = cva({
23
35
  },
24
36
  },
25
37
  defaultVariants: {
38
+ surface: 'outlined',
26
39
  disabled: 'false',
27
40
  error: 'false',
28
41
  },
@@ -35,9 +48,16 @@ export const composerFieldClass =
35
48
  /** Attachment tray above the field, inside the dock. */
36
49
  export const composerTrayClass = 'flex flex-wrap gap-2 px-3 pt-3'
37
50
 
38
- /** A single attachment chip. */
51
+ /**
52
+ * A single attachment chip. Chips animate in on add (scale + fade) and, when
53
+ * the adapter marks one `exiting`, animate out faster before unmounting. Both
54
+ * degrade to opacity-only / instant under reduced motion.
55
+ */
39
56
  export const composerAttachmentChipVariants = cva({
40
- base: 'inline-flex items-center gap-1 rounded-md bg-muted px-2 py-0.5 text-xs',
57
+ base: [
58
+ 'inline-flex items-center gap-1 rounded-md bg-muted px-2 py-0.5 text-xs',
59
+ 'origin-left transition-all duration-150 ease-out motion-reduce:transition-none',
60
+ ].join(' '),
41
61
  variants: {
42
62
  status: {
43
63
  pending: 'opacity-70',
@@ -45,9 +65,14 @@ export const composerAttachmentChipVariants = cva({
45
65
  ready: '',
46
66
  error: 'text-destructive',
47
67
  },
68
+ exiting: {
69
+ true: 'scale-90 opacity-0 motion-reduce:scale-100',
70
+ false: 'motion-safe:animate-fade-in-scale',
71
+ },
48
72
  },
49
73
  defaultVariants: {
50
74
  status: 'pending',
75
+ exiting: 'false',
51
76
  },
52
77
  })
53
78
 
@@ -55,9 +80,35 @@ export const composerAttachmentChipVariants = cva({
55
80
  export const composerTokenPillClass =
56
81
  'rounded bg-accent/40 px-0.5 text-accent-foreground'
57
82
 
58
- /** The caret-anchored suggestion menu. */
83
+ /**
84
+ * The caret-anchored suggestion menu. Enters with an eased fade+scale from its
85
+ * bottom edge (it sits above the field); reduced motion drops the movement.
86
+ */
59
87
  export const composerMenuClass =
60
- 'z-20 w-72 overflow-hidden rounded-xl border border-border bg-popover shadow-lg'
88
+ 'z-20 w-72 overflow-hidden rounded-xl border border-border bg-popover shadow-lg origin-bottom motion-safe:animate-fade-in-scale'
89
+
90
+ /**
91
+ * The inline expression/accessory panel docked below the field. It is part of
92
+ * the composer's own stack (never a floating portal), so the textarea and the
93
+ * message in progress stay fully visible while the panel is open. Opens with an
94
+ * eased fade; reduced motion shows it instantly.
95
+ */
96
+ export const composerAccessoryPanelClass =
97
+ 'border-t border-border motion-safe:animate-fade-in'
98
+
99
+ /** The built-in expression-panel toggle button. */
100
+ export const composerAccessoryToggleVariants = cva({
101
+ base: 'flex h-8 w-8 items-center justify-center rounded-lg transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
102
+ variants: {
103
+ active: {
104
+ true: 'bg-accent text-foreground',
105
+ false: 'text-muted-foreground',
106
+ },
107
+ },
108
+ defaultVariants: {
109
+ active: 'false',
110
+ },
111
+ })
61
112
 
62
113
  /** A suggestion row; `active` follows keyboard/pointer last-input-wins. */
63
114
  export const composerMenuItemVariants = cva({
@@ -87,9 +138,13 @@ export const composerCounterVariants = cva({
87
138
  },
88
139
  })
89
140
 
90
- /** Primary action (send ⇄ stop swap driven by `{hasText, canSend, isBusy}`). */
141
+ /**
142
+ * Primary action (send ⇄ stop swap driven by `{hasText, canSend, isBusy}`).
143
+ * The colour/affordance change eases; the adapter keys the button on busy so
144
+ * the swap pops in (scale) rather than snapping. Reduced motion keeps it calm.
145
+ */
91
146
  export const composerPrimaryActionVariants = cva({
92
- base: 'flex h-9 w-9 items-center justify-center rounded-full transition',
147
+ base: 'flex h-9 w-9 items-center justify-center rounded-full transition-all duration-150 ease-out active:scale-95 motion-reduce:transition-none motion-reduce:active:scale-100 motion-safe:animate-scale-in',
93
148
  variants: {
94
149
  enabled: {
95
150
  true: 'bg-primary text-primary-foreground hover:opacity-90',