@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,47 @@
1
+ // Languages
2
+ export type { Language } from './languages.js'
3
+ export {
4
+ LANGUAGES,
5
+ getLanguage,
6
+ getSupportedLanguages,
7
+ getLanguagesByGroup,
8
+ getLanguageName,
9
+ isRTL,
10
+ getTtsLang,
11
+ } from './languages.js'
12
+
13
+ // Countries
14
+ export type { Country } from './countries.js'
15
+ export {
16
+ COUNTRIES,
17
+ getCountry,
18
+ getCountryFlag,
19
+ getCountryName,
20
+ getDefaultLanguagesForCountry,
21
+ } from './countries.js'
22
+
23
+ // Region detection
24
+ export {
25
+ TIMEZONE_TO_COUNTRY,
26
+ LOCALE_TO_COUNTRY,
27
+ detectCountry,
28
+ detectLanguage,
29
+ } from './detect-region.js'
30
+
31
+ // Voice registry
32
+ export type { VoiceOption } from './voice-registry.js'
33
+ export {
34
+ VOICES,
35
+ getVoicesForLanguage,
36
+ getBestVoice,
37
+ getVoicesByProvider,
38
+ } from './voice-registry.js'
39
+
40
+ // Locale utilities
41
+ export {
42
+ LOCALE_DISPLAY_NAMES,
43
+ getLocaleDisplayName,
44
+ getStoredLocale,
45
+ setStoredLocale,
46
+ getLocalizedValue,
47
+ } from './locale-utils.js'
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Language type and registry for i18n support.
3
+ */
4
+
5
+ export interface Language {
6
+ code: string
7
+ name: string
8
+ nativeName: string
9
+ direction: 'ltr' | 'rtl'
10
+ group?: string
11
+ }
12
+
13
+ export const LANGUAGES: Record<string, Language> = {
14
+ // English variants
15
+ 'en': { code: 'en', name: 'English', nativeName: 'English', direction: 'ltr', group: 'English' },
16
+ 'en-US': { code: 'en-US', name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', group: 'English' },
17
+ 'en-GB': { code: 'en-GB', name: 'English (UK)', nativeName: 'English (UK)', direction: 'ltr', group: 'English' },
18
+ 'en-IN': { code: 'en-IN', name: 'English (India)', nativeName: 'English (India)',direction: 'ltr', group: 'English' },
19
+
20
+ // Indian languages
21
+ 'hi': { code: 'hi', name: 'Hindi', nativeName: 'हिन्दी', direction: 'ltr', group: 'Indian' },
22
+ 'te': { code: 'te', name: 'Telugu', nativeName: 'తెలుగు', direction: 'ltr', group: 'Indian' },
23
+ 'ta': { code: 'ta', name: 'Tamil', nativeName: 'தமிழ்', direction: 'ltr', group: 'Indian' },
24
+ 'ur': { code: 'ur', name: 'Urdu', nativeName: 'اردو', direction: 'rtl', group: 'Indian' },
25
+ 'pa': { code: 'pa', name: 'Punjabi', nativeName: 'ਪੰਜਾਬੀ', direction: 'ltr', group: 'Indian' },
26
+ 'bn': { code: 'bn', name: 'Bengali', nativeName: 'বাংলা', direction: 'ltr', group: 'Indian' },
27
+ 'gu': { code: 'gu', name: 'Gujarati', nativeName: 'ગુજરાતી', direction: 'ltr', group: 'Indian' },
28
+ 'mr': { code: 'mr', name: 'Marathi', nativeName: 'मराठी', direction: 'ltr', group: 'Indian' },
29
+ 'kn': { code: 'kn', name: 'Kannada', nativeName: 'ಕನ್ನಡ', direction: 'ltr', group: 'Indian' },
30
+ 'ml': { code: 'ml', name: 'Malayalam', nativeName: 'മലയാളം', direction: 'ltr', group: 'Indian' },
31
+
32
+ // Western European
33
+ 'fr': { code: 'fr', name: 'French', nativeName: 'Français', direction: 'ltr', group: 'European' },
34
+ 'es': { code: 'es', name: 'Spanish', nativeName: 'Español', direction: 'ltr', group: 'European' },
35
+ 'de': { code: 'de', name: 'German', nativeName: 'Deutsch', direction: 'ltr', group: 'European' },
36
+ 'it': { code: 'it', name: 'Italian', nativeName: 'Italiano', direction: 'ltr', group: 'European' },
37
+ 'pt': { code: 'pt', name: 'Portuguese', nativeName: 'Português', direction: 'ltr', group: 'European' },
38
+ 'pt-BR': { code: 'pt-BR', name: 'Portuguese (Brazil)', nativeName: 'Português (Brasil)', direction: 'ltr', group: 'European' },
39
+ 'nl': { code: 'nl', name: 'Dutch', nativeName: 'Nederlands', direction: 'ltr', group: 'European' },
40
+
41
+ // Nordic
42
+ 'sv': { code: 'sv', name: 'Swedish', nativeName: 'Svenska', direction: 'ltr', group: 'European' },
43
+ 'da': { code: 'da', name: 'Danish', nativeName: 'Dansk', direction: 'ltr', group: 'European' },
44
+ 'no': { code: 'no', name: 'Norwegian', nativeName: 'Norsk', direction: 'ltr', group: 'European' },
45
+ 'fi': { code: 'fi', name: 'Finnish', nativeName: 'Suomi', direction: 'ltr', group: 'European' },
46
+
47
+ // Eastern European
48
+ 'pl': { code: 'pl', name: 'Polish', nativeName: 'Polski', direction: 'ltr', group: 'European' },
49
+ 'cs': { code: 'cs', name: 'Czech', nativeName: 'Čeština', direction: 'ltr', group: 'European' },
50
+ 'ro': { code: 'ro', name: 'Romanian', nativeName: 'Română', direction: 'ltr', group: 'European' },
51
+ 'hu': { code: 'hu', name: 'Hungarian', nativeName: 'Magyar', direction: 'ltr', group: 'European' },
52
+ 'bg': { code: 'bg', name: 'Bulgarian', nativeName: 'Български', direction: 'ltr', group: 'European' },
53
+ 'hr': { code: 'hr', name: 'Croatian', nativeName: 'Hrvatski', direction: 'ltr', group: 'European' },
54
+ 'sk': { code: 'sk', name: 'Slovak', nativeName: 'Slovenčina', direction: 'ltr', group: 'European' },
55
+ 'sl': { code: 'sl', name: 'Slovenian', nativeName: 'Slovenščina', direction: 'ltr', group: 'European' },
56
+ 'sr': { code: 'sr', name: 'Serbian', nativeName: 'Srpski', direction: 'ltr', group: 'European' },
57
+ 'uk': { code: 'uk', name: 'Ukrainian', nativeName: 'Українська', direction: 'ltr', group: 'European' },
58
+ 'el': { code: 'el', name: 'Greek', nativeName: 'Ελληνικά', direction: 'ltr', group: 'European' },
59
+ 'ru': { code: 'ru', name: 'Russian', nativeName: 'Русский', direction: 'ltr', group: 'European' },
60
+
61
+ // East Asian
62
+ 'ja': { code: 'ja', name: 'Japanese', nativeName: '日本語', direction: 'ltr', group: 'East Asian' },
63
+ 'ko': { code: 'ko', name: 'Korean', nativeName: '한국어', direction: 'ltr', group: 'East Asian' },
64
+ 'zh': { code: 'zh', name: 'Chinese (Simplified)', nativeName: '中文(简体)', direction: 'ltr', group: 'East Asian' },
65
+ 'zh-TW': { code: 'zh-TW', name: 'Chinese (Traditional)', nativeName: '中文(繁體)', direction: 'ltr', group: 'East Asian' },
66
+
67
+ // Middle Eastern
68
+ 'ar': { code: 'ar', name: 'Arabic', nativeName: 'العربية', direction: 'rtl', group: 'Middle Eastern' },
69
+ 'he': { code: 'he', name: 'Hebrew', nativeName: 'עברית', direction: 'rtl', group: 'Middle Eastern' },
70
+ 'tr': { code: 'tr', name: 'Turkish', nativeName: 'Türkçe', direction: 'ltr', group: 'Middle Eastern' },
71
+
72
+ // Southeast Asian
73
+ 'th': { code: 'th', name: 'Thai', nativeName: 'ไทย', direction: 'ltr', group: 'Southeast Asian' },
74
+ 'vi': { code: 'vi', name: 'Vietnamese', nativeName: 'Tiếng Việt', direction: 'ltr', group: 'Southeast Asian' },
75
+ 'id': { code: 'id', name: 'Indonesian', nativeName: 'Bahasa Indonesia', direction: 'ltr', group: 'Southeast Asian' },
76
+ 'ms': { code: 'ms', name: 'Malay', nativeName: 'Bahasa Melayu', direction: 'ltr', group: 'Southeast Asian' },
77
+ }
78
+
79
+ /** BCP-47 TTS language tag mappings for base language codes */
80
+ const TTS_LANG_MAP: Record<string, string> = {
81
+ 'en': 'en-US',
82
+ 'en-US': 'en-US',
83
+ 'en-GB': 'en-GB',
84
+ 'en-IN': 'en-IN',
85
+ 'hi': 'hi-IN',
86
+ 'te': 'te-IN',
87
+ 'ta': 'ta-IN',
88
+ 'ur': 'ur-PK',
89
+ 'pa': 'pa-IN',
90
+ 'bn': 'bn-IN',
91
+ 'gu': 'gu-IN',
92
+ 'mr': 'mr-IN',
93
+ 'kn': 'kn-IN',
94
+ 'ml': 'ml-IN',
95
+ 'fr': 'fr-FR',
96
+ 'es': 'es-ES',
97
+ 'de': 'de-DE',
98
+ 'it': 'it-IT',
99
+ 'pt': 'pt-PT',
100
+ 'pt-BR': 'pt-BR',
101
+ 'nl': 'nl-NL',
102
+ 'sv': 'sv-SE',
103
+ 'da': 'da-DK',
104
+ 'no': 'nb-NO',
105
+ 'fi': 'fi-FI',
106
+ 'pl': 'pl-PL',
107
+ 'cs': 'cs-CZ',
108
+ 'ro': 'ro-RO',
109
+ 'hu': 'hu-HU',
110
+ 'bg': 'bg-BG',
111
+ 'hr': 'hr-HR',
112
+ 'sk': 'sk-SK',
113
+ 'sl': 'sl-SI',
114
+ 'sr': 'sr-RS',
115
+ 'uk': 'uk-UA',
116
+ 'el': 'el-GR',
117
+ 'ru': 'ru-RU',
118
+ 'ja': 'ja-JP',
119
+ 'ko': 'ko-KR',
120
+ 'zh': 'zh-CN',
121
+ 'zh-TW': 'zh-TW',
122
+ 'ar': 'ar-SA',
123
+ 'he': 'he-IL',
124
+ 'tr': 'tr-TR',
125
+ 'th': 'th-TH',
126
+ 'vi': 'vi-VN',
127
+ 'id': 'id-ID',
128
+ 'ms': 'ms-MY',
129
+ }
130
+
131
+ /**
132
+ * Get a language by its code.
133
+ */
134
+ export function getLanguage(code: string): Language | undefined {
135
+ return LANGUAGES[code]
136
+ }
137
+
138
+ /**
139
+ * Get all supported languages as an array.
140
+ */
141
+ export function getSupportedLanguages(): Language[] {
142
+ return Object.values(LANGUAGES)
143
+ }
144
+
145
+ /**
146
+ * Get languages grouped by their language family/group.
147
+ */
148
+ export function getLanguagesByGroup(): Record<string, Language[]> {
149
+ const groups: Record<string, Language[]> = {}
150
+ for (const lang of Object.values(LANGUAGES)) {
151
+ const group = lang.group ?? 'Other'
152
+ if (!groups[group]) {
153
+ groups[group] = []
154
+ }
155
+ groups[group].push(lang)
156
+ }
157
+ return groups
158
+ }
159
+
160
+ /**
161
+ * Get the display name for a language code.
162
+ * Returns empty string if the code is not found.
163
+ */
164
+ export function getLanguageName(code: string): string {
165
+ return LANGUAGES[code]?.name ?? ''
166
+ }
167
+
168
+ /**
169
+ * Check if a language code represents a right-to-left language.
170
+ */
171
+ export function isRTL(code: string): boolean {
172
+ return LANGUAGES[code]?.direction === 'rtl'
173
+ }
174
+
175
+ /**
176
+ * Get the BCP-47 language tag suitable for TTS engines.
177
+ * Returns the code itself if no mapping exists.
178
+ */
179
+ export function getTtsLang(code: string): string {
180
+ return TTS_LANG_MAP[code] ?? code
181
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Locale utilities for display names, storage, and localized value resolution.
3
+ */
4
+
5
+ /** Display names for common locale codes (in their native language) */
6
+ export const LOCALE_DISPLAY_NAMES: Record<string, string> = {
7
+ 'en': 'English',
8
+ 'en-US': 'English (US)',
9
+ 'en-GB': 'English (UK)',
10
+ 'en-IN': 'English (India)',
11
+ 'hi': 'हिन्दी',
12
+ 'te': 'తెలుగు',
13
+ 'ta': 'தமிழ்',
14
+ 'ur': 'اردو',
15
+ 'pa': 'ਪੰਜਾਬੀ',
16
+ 'bn': 'বাংলা',
17
+ 'gu': 'ગુજરાતી',
18
+ 'mr': 'मराठी',
19
+ 'kn': 'ಕನ್ನಡ',
20
+ 'ml': 'മലയാളം',
21
+ 'fr': 'Français',
22
+ 'es': 'Español',
23
+ 'de': 'Deutsch',
24
+ 'it': 'Italiano',
25
+ 'pt': 'Português',
26
+ 'pt-BR': 'Português (Brasil)',
27
+ 'nl': 'Nederlands',
28
+ 'sv': 'Svenska',
29
+ 'da': 'Dansk',
30
+ 'no': 'Norsk',
31
+ 'fi': 'Suomi',
32
+ 'pl': 'Polski',
33
+ 'cs': 'Čeština',
34
+ 'ro': 'Română',
35
+ 'hu': 'Magyar',
36
+ 'bg': 'Български',
37
+ 'hr': 'Hrvatski',
38
+ 'sk': 'Slovenčina',
39
+ 'sl': 'Slovenščina',
40
+ 'sr': 'Srpski',
41
+ 'uk': 'Українська',
42
+ 'el': 'Ελληνικά',
43
+ 'ru': 'Русский',
44
+ 'ja': '日本語',
45
+ 'ko': '한국어',
46
+ 'zh': '中文',
47
+ 'zh-TW': '中文(繁體)',
48
+ 'ar': 'العربية',
49
+ 'he': 'עברית',
50
+ 'tr': 'Türkçe',
51
+ 'th': 'ไทย',
52
+ 'vi': 'Tiếng Việt',
53
+ 'id': 'Bahasa Indonesia',
54
+ 'ms': 'Bahasa Melayu',
55
+ }
56
+
57
+ const DEFAULT_STORAGE_KEY = 'refraction-ui-locale'
58
+
59
+ /**
60
+ * Get the display name for a locale code.
61
+ * Returns the code itself if not found in the display names map.
62
+ */
63
+ export function getLocaleDisplayName(code: string): string {
64
+ return LOCALE_DISPLAY_NAMES[code] ?? code
65
+ }
66
+
67
+ /**
68
+ * Get the stored locale from localStorage (or compatible storage).
69
+ * Returns null if not stored or localStorage is not available.
70
+ */
71
+ export function getStoredLocale(storageKey?: string): string | null {
72
+ try {
73
+ if (typeof localStorage !== 'undefined') {
74
+ return localStorage.getItem(storageKey ?? DEFAULT_STORAGE_KEY)
75
+ }
76
+ } catch {
77
+ // localStorage may not be available
78
+ }
79
+ return null
80
+ }
81
+
82
+ /**
83
+ * Store a locale code in localStorage (or compatible storage).
84
+ * Does nothing if localStorage is not available.
85
+ */
86
+ export function setStoredLocale(code: string, storageKey?: string): void {
87
+ try {
88
+ if (typeof localStorage !== 'undefined') {
89
+ localStorage.setItem(storageKey ?? DEFAULT_STORAGE_KEY, code)
90
+ }
91
+ } catch {
92
+ // localStorage may not be available
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Resolve a localized value from a record of locale -> value.
98
+ * Tries exact locale, then base language code (for regional variants like 'en-US' -> 'en'),
99
+ * then the fallback locale.
100
+ */
101
+ export function getLocalizedValue<T>(
102
+ localized: Record<string, T>,
103
+ locale: string,
104
+ fallbackLocale?: string,
105
+ ): T | undefined {
106
+ // 1. Exact match
107
+ if (locale in localized) {
108
+ return localized[locale]
109
+ }
110
+
111
+ // 2. Try base language code (e.g., 'en-US' -> 'en')
112
+ const baseLang = locale.split('-')[0]
113
+ if (baseLang !== locale && baseLang in localized) {
114
+ return localized[baseLang]
115
+ }
116
+
117
+ // 3. Fallback locale
118
+ if (fallbackLocale && fallbackLocale in localized) {
119
+ return localized[fallbackLocale]
120
+ }
121
+
122
+ return undefined
123
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Voice registry for TTS providers.
3
+ */
4
+
5
+ export interface VoiceOption {
6
+ id: string
7
+ name: string
8
+ language: string
9
+ provider: 'browser' | 'google' | 'elevenlabs' | 'openai'
10
+ gender?: 'male' | 'female'
11
+ }
12
+
13
+ export const VOICES: VoiceOption[] = [
14
+ // Browser voices — English
15
+ { id: 'browser-en-us-female', name: 'English (US) Female', language: 'en-US', provider: 'browser', gender: 'female' },
16
+ { id: 'browser-en-us-male', name: 'English (US) Male', language: 'en-US', provider: 'browser', gender: 'male' },
17
+ { id: 'browser-en-gb-female', name: 'English (UK) Female', language: 'en-GB', provider: 'browser', gender: 'female' },
18
+ { id: 'browser-en-gb-male', name: 'English (UK) Male', language: 'en-GB', provider: 'browser', gender: 'male' },
19
+ { id: 'browser-en-in-female', name: 'English (India) Female', language: 'en-IN', provider: 'browser', gender: 'female' },
20
+
21
+ // Browser voices — European
22
+ { id: 'browser-fr-female', name: 'French Female', language: 'fr', provider: 'browser', gender: 'female' },
23
+ { id: 'browser-de-female', name: 'German Female', language: 'de', provider: 'browser', gender: 'female' },
24
+ { id: 'browser-es-female', name: 'Spanish Female', language: 'es', provider: 'browser', gender: 'female' },
25
+ { id: 'browser-it-female', name: 'Italian Female', language: 'it', provider: 'browser', gender: 'female' },
26
+ { id: 'browser-pt-female', name: 'Portuguese Female', language: 'pt', provider: 'browser', gender: 'female' },
27
+
28
+ // Browser voices — Asian
29
+ { id: 'browser-ja-female', name: 'Japanese Female', language: 'ja', provider: 'browser', gender: 'female' },
30
+ { id: 'browser-ko-female', name: 'Korean Female', language: 'ko', provider: 'browser', gender: 'female' },
31
+ { id: 'browser-zh-female', name: 'Chinese Female', language: 'zh', provider: 'browser', gender: 'female' },
32
+ { id: 'browser-hi-female', name: 'Hindi Female', language: 'hi', provider: 'browser', gender: 'female' },
33
+ { id: 'browser-ar-male', name: 'Arabic Male', language: 'ar', provider: 'browser', gender: 'male' },
34
+
35
+ // Google TTS voices
36
+ { id: 'google-en-us-wavenet-d', name: 'Google WaveNet English (US)', language: 'en-US', provider: 'google', gender: 'male' },
37
+ { id: 'google-en-us-wavenet-f', name: 'Google WaveNet English (US) Female', language: 'en-US', provider: 'google', gender: 'female' },
38
+ { id: 'google-en-gb-wavenet-a', name: 'Google WaveNet English (UK)', language: 'en-GB', provider: 'google', gender: 'female' },
39
+ { id: 'google-fr-wavenet-a', name: 'Google WaveNet French', language: 'fr', provider: 'google', gender: 'female' },
40
+ { id: 'google-de-wavenet-a', name: 'Google WaveNet German', language: 'de', provider: 'google', gender: 'female' },
41
+ { id: 'google-es-wavenet-a', name: 'Google WaveNet Spanish', language: 'es', provider: 'google', gender: 'female' },
42
+ { id: 'google-ja-wavenet-a', name: 'Google WaveNet Japanese', language: 'ja', provider: 'google', gender: 'female' },
43
+ { id: 'google-hi-wavenet-a', name: 'Google WaveNet Hindi', language: 'hi', provider: 'google', gender: 'female' },
44
+ { id: 'google-ar-wavenet-a', name: 'Google WaveNet Arabic', language: 'ar', provider: 'google', gender: 'female' },
45
+
46
+ // ElevenLabs voices
47
+ { id: 'elevenlabs-rachel', name: 'Rachel', language: 'en', provider: 'elevenlabs', gender: 'female' },
48
+ { id: 'elevenlabs-adam', name: 'Adam', language: 'en', provider: 'elevenlabs', gender: 'male' },
49
+ { id: 'elevenlabs-antoni', name: 'Antoni', language: 'en', provider: 'elevenlabs', gender: 'male' },
50
+ { id: 'elevenlabs-bella', name: 'Bella', language: 'es', provider: 'elevenlabs', gender: 'female' },
51
+ { id: 'elevenlabs-elli', name: 'Elli', language: 'de', provider: 'elevenlabs', gender: 'female' },
52
+
53
+ // OpenAI TTS voices
54
+ { id: 'openai-alloy', name: 'Alloy', language: 'en', provider: 'openai' },
55
+ { id: 'openai-echo', name: 'Echo', language: 'en', provider: 'openai', gender: 'male' },
56
+ { id: 'openai-fable', name: 'Fable', language: 'en', provider: 'openai', gender: 'male' },
57
+ { id: 'openai-onyx', name: 'Onyx', language: 'en', provider: 'openai', gender: 'male' },
58
+ { id: 'openai-nova', name: 'Nova', language: 'en', provider: 'openai', gender: 'female' },
59
+ { id: 'openai-shimmer', name: 'Shimmer', language: 'en', provider: 'openai', gender: 'female' },
60
+ ]
61
+
62
+ /**
63
+ * Get all voices that match a given language code.
64
+ * Matches both exact codes and prefix codes (e.g., 'en' matches 'en', 'en-US', 'en-GB').
65
+ */
66
+ export function getVoicesForLanguage(langCode: string): VoiceOption[] {
67
+ return VOICES.filter((v) => v.language === langCode || v.language.startsWith(langCode + '-') || langCode.startsWith(v.language + '-') || v.language.split('-')[0] === langCode)
68
+ }
69
+
70
+ /**
71
+ * Get the best voice for a language, optionally preferring a specific provider.
72
+ * Returns undefined if no voice matches.
73
+ */
74
+ export function getBestVoice(langCode: string, preferredProvider?: VoiceOption['provider']): VoiceOption | undefined {
75
+ const voices = getVoicesForLanguage(langCode)
76
+ if (voices.length === 0) return undefined
77
+
78
+ if (preferredProvider) {
79
+ const preferred = voices.find((v) => v.provider === preferredProvider)
80
+ if (preferred) return preferred
81
+ }
82
+
83
+ // Return first available voice
84
+ return voices[0]
85
+ }
86
+
87
+ /**
88
+ * Get all voices from a specific provider.
89
+ */
90
+ export function getVoicesByProvider(provider: VoiceOption['provider']): VoiceOption[] {
91
+ return VOICES.filter((v) => v.provider === provider)
92
+ }
package/dist/index.ts CHANGED
@@ -64,6 +64,7 @@ export * from './astro-link-card/index.ts';
64
64
  export * from './astro-live-captions/index.ts';
65
65
  export * from './astro-live-cursors/index.ts';
66
66
  export * from './astro-live-transcript/index.ts';
67
+ export * from './astro-location-selector/index.ts';
67
68
  export * from './astro-logger/index.ts';
68
69
  export * from './astro-markdown-renderer/index.ts';
69
70
  export * from './astro-marquee-strip/index.ts';
@@ -0,0 +1 @@
1
+ export * from './location-selector.ts'
@@ -0,0 +1,60 @@
1
+ import { COUNTRIES, LANGUAGES, detectCountry, detectLanguage } from '../i18n/index.ts'
2
+ import { generateId } from '../shared/index.ts'
3
+
4
+ export interface LocationSelectorState {
5
+ country: string
6
+ language: string
7
+ }
8
+
9
+ export interface LocationSelectorOptions {
10
+ defaultCountry?: string
11
+ defaultLanguage?: string
12
+ onCountryChange?: (country: string) => void
13
+ onLanguageChange?: (language: string) => void
14
+ }
15
+
16
+ export function createLocationSelector(options: LocationSelectorOptions = {}) {
17
+ const {
18
+ defaultCountry = 'US',
19
+ defaultLanguage = 'en',
20
+ onCountryChange,
21
+ onLanguageChange,
22
+ } = options
23
+
24
+ let state: LocationSelectorState = {
25
+ country: defaultCountry,
26
+ language: defaultLanguage,
27
+ }
28
+
29
+ const countryId = generateId('location-sel-country')
30
+ const languageId = generateId('location-sel-language')
31
+
32
+ function setCountry(country: string) {
33
+ state.country = country
34
+ onCountryChange?.(country)
35
+ }
36
+
37
+ function setLanguage(language: string) {
38
+ state.language = language
39
+ onLanguageChange?.(language)
40
+ }
41
+
42
+ const countries = Array.isArray(COUNTRIES) ? COUNTRIES : Object.values(COUNTRIES)
43
+ const languages = Array.isArray(LANGUAGES) ? LANGUAGES : Object.values(LANGUAGES)
44
+
45
+ return {
46
+ state,
47
+ setCountry,
48
+ setLanguage,
49
+ countries,
50
+ languages,
51
+ countryProps: {
52
+ id: countryId,
53
+ name: 'country',
54
+ },
55
+ languageProps: {
56
+ id: languageId,
57
+ name: 'language',
58
+ },
59
+ }
60
+ }
@@ -1,127 +1,21 @@
1
1
  /**
2
2
  * Emoji system — shortcode to unicode mapping and search.
3
+ *
4
+ * The map is derived from the single shared emoji dataset in
5
+ * `@refraction-ui/emoji-picker` (`buildShortcodeMap`), so rich-editor, the
6
+ * emoji picker, and the composer's `:` trigger all resolve `:shortcode:` the
7
+ * same way — the ~120-entry hand-list that used to live here (and diverge from
8
+ * the picker) is gone. Popular aliases (`:smile:`, `:joy:`, `:100:`) live in
9
+ * the shared dataset's `SHORTCODE_ALIASES`.
3
10
  */
11
+ import { buildShortcodeMap } from '../emoji-picker/index.ts'
4
12
 
5
13
  // ---------------------------------------------------------------------------
6
14
  // Emoji map
7
15
  // ---------------------------------------------------------------------------
8
16
 
9
- export const EMOJI_MAP: Record<string, string> = {
10
- ':smile:': '\u{1F604}',
11
- ':laugh:': '\u{1F602}',
12
- ':joy:': '\u{1F602}',
13
- ':grin:': '\u{1F601}',
14
- ':wink:': '\u{1F609}',
15
- ':blush:': '\u{1F60A}',
16
- ':heart_eyes:': '\u{1F60D}',
17
- ':kissing:': '\u{1F617}',
18
- ':thinking:': '\u{1F914}',
19
- ':neutral:': '\u{1F610}',
20
- ':expressionless:': '\u{1F611}',
21
- ':unamused:': '\u{1F612}',
22
- ':sweat:': '\u{1F613}',
23
- ':pensive:': '\u{1F614}',
24
- ':confused:': '\u{1F615}',
25
- ':disappointed:': '\u{1F61E}',
26
- ':worried:': '\u{1F61F}',
27
- ':angry:': '\u{1F620}',
28
- ':rage:': '\u{1F621}',
29
- ':cry:': '\u{1F622}',
30
- ':sob:': '\u{1F62D}',
31
- ':scream:': '\u{1F631}',
32
- ':fearful:': '\u{1F628}',
33
- ':cold_sweat:': '\u{1F630}',
34
- ':relieved:': '\u{1F60C}',
35
- ':sleepy:': '\u{1F62A}',
36
- ':sleeping:': '\u{1F634}',
37
- ':mask:': '\u{1F637}',
38
- ':sunglasses:': '\u{1F60E}',
39
- ':nerd:': '\u{1F913}',
40
- ':heart:': '\u{2764}\u{FE0F}',
41
- ':broken_heart:': '\u{1F494}',
42
- ':sparkling_heart:': '\u{1F496}',
43
- ':two_hearts:': '\u{1F495}',
44
- ':fire:': '\u{1F525}',
45
- ':100:': '\u{1F4AF}',
46
- ':star:': '\u{2B50}',
47
- ':star2:': '\u{1F31F}',
48
- ':sparkles:': '\u{2728}',
49
- ':thumbsup:': '\u{1F44D}',
50
- ':thumbsdown:': '\u{1F44E}',
51
- ':wave:': '\u{1F44B}',
52
- ':clap:': '\u{1F44F}',
53
- ':raised_hands:': '\u{1F64C}',
54
- ':pray:': '\u{1F64F}',
55
- ':muscle:': '\u{1F4AA}',
56
- ':point_up:': '\u{261D}\u{FE0F}',
57
- ':point_down:': '\u{1F447}',
58
- ':point_left:': '\u{1F448}',
59
- ':point_right:': '\u{1F449}',
60
- ':ok_hand:': '\u{1F44C}',
61
- ':v:': '\u{270C}\u{FE0F}',
62
- ':eyes:': '\u{1F440}',
63
- ':tongue:': '\u{1F445}',
64
- ':lips:': '\u{1F444}',
65
- ':rocket:': '\u{1F680}',
66
- ':tada:': '\u{1F389}',
67
- ':confetti:': '\u{1F38A}',
68
- ':balloon:': '\u{1F388}',
69
- ':gift:': '\u{1F381}',
70
- ':trophy:': '\u{1F3C6}',
71
- ':medal:': '\u{1F3C5}',
72
- ':crown:': '\u{1F451}',
73
- ':gem:': '\u{1F48E}',
74
- ':bell:': '\u{1F514}',
75
- ':check:': '\u{2705}',
76
- ':x:': '\u{274C}',
77
- ':warning:': '\u{26A0}\u{FE0F}',
78
- ':no_entry:': '\u{26D4}',
79
- ':question:': '\u{2753}',
80
- ':exclamation:': '\u{2757}',
81
- ':bulb:': '\u{1F4A1}',
82
- ':book:': '\u{1F4D6}',
83
- ':memo:': '\u{1F4DD}',
84
- ':pencil:': '\u{270F}\u{FE0F}',
85
- ':calendar:': '\u{1F4C5}',
86
- ':clock:': '\u{1F550}',
87
- ':hourglass:': '\u{231B}',
88
- ':lock:': '\u{1F512}',
89
- ':key:': '\u{1F511}',
90
- ':hammer:': '\u{1F528}',
91
- ':wrench:': '\u{1F527}',
92
- ':link:': '\u{1F517}',
93
- ':paperclip:': '\u{1F4CE}',
94
- ':scissors:': '\u{2702}\u{FE0F}',
95
- ':package:': '\u{1F4E6}',
96
- ':email:': '\u{1F4E7}',
97
- ':phone:': '\u{1F4DE}',
98
- ':computer:': '\u{1F4BB}',
99
- ':house:': '\u{1F3E0}',
100
- ':tree:': '\u{1F333}',
101
- ':sun:': '\u{2600}\u{FE0F}',
102
- ':moon:': '\u{1F319}',
103
- ':cloud:': '\u{2601}\u{FE0F}',
104
- ':rain:': '\u{1F327}\u{FE0F}',
105
- ':snow:': '\u{2744}\u{FE0F}',
106
- ':zap:': '\u{26A1}',
107
- ':rainbow:': '\u{1F308}',
108
- ':pizza:': '\u{1F355}',
109
- ':hamburger:': '\u{1F354}',
110
- ':coffee:': '\u{2615}',
111
- ':beer:': '\u{1F37A}',
112
- ':wine:': '\u{1F377}',
113
- ':cake:': '\u{1F370}',
114
- ':dog:': '\u{1F436}',
115
- ':cat:': '\u{1F431}',
116
- ':poop:': '\u{1F4A9}',
117
- ':ghost:': '\u{1F47B}',
118
- ':skull:': '\u{1F480}',
119
- ':alien:': '\u{1F47D}',
120
- ':robot:': '\u{1F916}',
121
- ':handshake:': '\u{1F91D}',
122
- ':pray2:': '\u{1F64F}',
123
- ':salute:': '\u{1FAE1}',
124
- }
17
+ /** `:shortcode:` → glyph, built from the shared dataset (full set + aliases). */
18
+ export const EMOJI_MAP: Record<string, string> = buildShortcodeMap()
125
19
 
126
20
  // ---------------------------------------------------------------------------
127
21
  // Detection and search
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@refraction-ui/astro",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
4
4
  "description": "All Refraction UI Astro components in one package",
5
5
  "type": "module",
6
6
  "exports": {
@@ -84,6 +84,7 @@
84
84
  "@refraction-ui/astro-live-captions": "workspace:*",
85
85
  "@refraction-ui/astro-live-cursors": "workspace:*",
86
86
  "@refraction-ui/astro-live-transcript": "workspace:*",
87
+ "@refraction-ui/astro-location-selector": "workspace:*",
87
88
  "@refraction-ui/astro-logger": "workspace:*",
88
89
  "@refraction-ui/astro-markdown-renderer": "workspace:*",
89
90
  "@refraction-ui/astro-marquee-strip": "workspace:*",