@xyhp915/slack-base-ui 0.0.4 → 0.0.5

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.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@xyhp915/slack-base-ui",
3
3
  "main": "libs/index.js",
4
4
  "types": "libs/index.d.ts",
5
- "version": "0.0.4",
5
+ "version": "0.0.5",
6
6
  "type": "module",
7
7
  "scripts": {
8
8
  "dev": "vite",
@@ -1,4 +1,4 @@
1
- import React from 'react'
1
+ import React, { createContext, useContext, useState } from 'react'
2
2
  import { Toast as BaseToast } from '@base-ui/react'
3
3
  import clsx from 'clsx'
4
4
  import { X, Info, CheckCircle, AlertTriangle, XCircle } from 'lucide-react'
@@ -7,27 +7,71 @@ import { X, Info, CheckCircle, AlertTriangle, XCircle } from 'lucide-react'
7
7
 
8
8
  export type ToastType = 'default' | 'info' | 'success' | 'warning' | 'error'
9
9
 
10
+ export type ToastPosition =
11
+ | 'top-left'
12
+ | 'top-center'
13
+ | 'top-right'
14
+ | 'bottom-left'
15
+ | 'bottom-center'
16
+ | 'bottom-right'
17
+
18
+ // ── Position context ──────────────────────────────────────────────────────────
19
+
20
+ interface ToastPositionState {
21
+ position: ToastPosition
22
+ setPosition: (p: ToastPosition) => void
23
+ }
24
+
25
+ const ToastPositionContext = createContext<ToastPositionState>({
26
+ position: 'bottom-right',
27
+ setPosition: () => {},
28
+ })
29
+
30
+ const positionClasses: Record<ToastPosition, string> = {
31
+ 'top-left': 'top-4 left-4 flex-col',
32
+ 'top-center': 'top-4 left-1/2 -translate-x-1/2 flex-col',
33
+ 'top-right': 'top-4 right-4 flex-col',
34
+ 'bottom-left': 'bottom-4 left-4 flex-col-reverse',
35
+ 'bottom-center':'bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse',
36
+ 'bottom-right': 'bottom-4 right-4 flex-col-reverse',
37
+ }
38
+
10
39
  // ── ToastProvider ─────────────────────────────────────────────────────────────
11
40
 
12
41
  export interface ToastProviderProps {
13
42
  children: React.ReactNode
14
43
  timeout?: number
15
44
  limit?: number
45
+ /** Where toasts appear on screen. Defaults to `'bottom-right'`. */
46
+ position?: ToastPosition
16
47
  }
17
48
 
18
49
  /**
19
50
  * Wrap your app (or part of it) with `ToastProvider` to enable toasts.
20
51
  * Then use the `useToast()` hook inside to add toasts.
52
+ *
53
+ * @example
54
+ * // Positon can be set at provider level:
55
+ * <ToastProvider position="top-center">...</ToastProvider>
56
+ *
57
+ * // Or changed at runtime via the hook:
58
+ * const { setPosition } = useToast()
59
+ * setPosition('top-right')
21
60
  */
22
61
  export const ToastProvider: React.FC<ToastProviderProps> = ({
23
62
  children,
24
63
  timeout = 5000,
25
64
  limit = 5,
65
+ position: defaultPosition = 'bottom-right',
26
66
  }) => {
67
+ const [position, setPosition] = useState<ToastPosition>(defaultPosition)
68
+
27
69
  return (
28
70
  <BaseToast.Provider timeout={timeout} limit={limit}>
29
- {children}
30
- <ToastViewport />
71
+ <ToastPositionContext.Provider value={{ position, setPosition }}>
72
+ {children}
73
+ <ToastViewport />
74
+ </ToastPositionContext.Provider>
31
75
  </BaseToast.Provider>
32
76
  )
33
77
  }
@@ -48,11 +92,14 @@ export interface ToastOptions {
48
92
 
49
93
  /**
50
94
  * Hook to imperatively add, close and update toasts.
95
+ * Also exposes `position` / `setPosition` for runtime position changes.
51
96
  *
52
97
  * Must be used inside `<ToastProvider>`.
53
98
  */
54
99
  export function useToast() {
55
100
  const manager = BaseToast.useToastManager()
101
+ const { position, setPosition } = useContext(ToastPositionContext)
102
+
56
103
  return {
57
104
  toast: (options: ToastOptions) =>
58
105
  manager.add({
@@ -65,6 +112,10 @@ export function useToast() {
65
112
  : undefined,
66
113
  }),
67
114
  dismiss: manager.close,
115
+ /** Current toast position */
116
+ position,
117
+ /** Dynamically change where toasts appear */
118
+ setPosition,
68
119
  }
69
120
  }
70
121
 
@@ -72,9 +123,15 @@ export function useToast() {
72
123
 
73
124
  function ToastViewport() {
74
125
  const { toasts } = BaseToast.useToastManager()
126
+ const { position } = useContext(ToastPositionContext)
75
127
 
76
128
  return (
77
- <BaseToast.Viewport className="fixed bottom-4 right-4 z-[100] flex w-80 flex-col-reverse gap-2 outline-none">
129
+ <BaseToast.Viewport
130
+ className={clsx(
131
+ 'fixed z-[100] flex w-80 gap-2 outline-none',
132
+ positionClasses[position],
133
+ )}
134
+ >
78
135
  {toasts.map((toast) => (
79
136
  <ToastItem key={toast.id} toast={toast} />
80
137
  ))}
@@ -84,49 +141,94 @@ function ToastViewport() {
84
141
 
85
142
  // ── ToastItem (internal) ──────────────────────────────────────────────────────
86
143
 
87
- const typeStyles: Record<ToastType, { icon: React.ReactNode; color: string }> = {
88
- default: { icon: null, color: 'border-(--border-light)' },
144
+ /**
145
+ * Per-type config — icon colors adapt to theme via dark: modifier.
146
+ * Accent strip colors stay the same in both modes.
147
+ */
148
+ const typeConfig: Record<
149
+ ToastType,
150
+ {
151
+ icon: React.ReactNode
152
+ titleClass: string
153
+ surfaceClass: string
154
+ actionClass: string
155
+ closeClass: string
156
+ }
157
+ > = {
158
+ default: {
159
+ icon: null,
160
+ titleClass: 'text-(--text-primary) dark:text-white',
161
+ surfaceClass: 'bg-(--bg-primary) border-(--border-light) dark:bg-[#2c2f33] dark:border-transparent',
162
+ actionClass: 'text-(--accent) hover:underline dark:text-white/85 dark:hover:text-white',
163
+ closeClass: 'text-(--text-muted) hover:bg-(--bg-hover) hover:text-(--text-primary) dark:text-white/40 dark:hover:bg-white/10 dark:hover:text-white',
164
+ },
89
165
  info: {
90
- icon: <Info size={16} className="shrink-0 text-blue-500" />,
91
- color: 'border-blue-200 dark:border-blue-900',
166
+ icon: <Info size={14} className="text-[#1164A3] dark:text-[#5ba4cf]" />,
167
+ titleClass: 'text-[#1164A3] dark:text-[#5ba4cf]',
168
+ surfaceClass: 'bg-[#F6FAFE] border-[#E3EFF9] dark:bg-[#102633] dark:border-[#1A4256]',
169
+ actionClass: 'text-[#0F5A94] hover:underline hover:text-[#0B4673] dark:text-[#7AB7DE] dark:hover:text-[#A7D1EC]',
170
+ closeClass: 'text-[#7C9BB4] hover:bg-[#E8F2FB] hover:text-[#1164A3] dark:text-[#8AA7BD] dark:hover:bg-[#173547] dark:hover:text-[#7AB7DE]',
92
171
  },
93
172
  success: {
94
- icon: <CheckCircle size={16} className="shrink-0 text-(--slack-green)" />,
95
- color: 'border-green-200 dark:border-green-900',
173
+ icon: <CheckCircle size={14} className="text-[#007a5a] dark:text-[#39c088]" />,
174
+ titleClass: 'text-[#007a5a] dark:text-[#39c088]',
175
+ surfaceClass: 'bg-[#F4FBF7] border-[#DFF1E8] dark:bg-[#102A20] dark:border-[#1A4B39]',
176
+ actionClass: 'text-[#0A6B51] hover:underline hover:text-[#08533F] dark:text-[#6FD4A3] dark:hover:text-[#96E6BB]',
177
+ closeClass: 'text-[#7A9E91] hover:bg-[#E7F6EE] hover:text-[#007A5A] dark:text-[#89A99E] dark:hover:bg-[#18372C] dark:hover:text-[#6FD4A3]',
96
178
  },
97
179
  warning: {
98
- icon: <AlertTriangle size={16} className="shrink-0 text-amber-500" />,
99
- color: 'border-amber-200 dark:border-amber-900',
180
+ icon: <AlertTriangle size={14} className="text-amber-500 dark:text-amber-400" />,
181
+ titleClass: 'text-amber-500 dark:text-amber-400',
182
+ surfaceClass: 'bg-[#FFFBF0] border-[#F7E9BF] dark:bg-[#3A2A09] dark:border-[#5C4514]',
183
+ actionClass: 'text-[#B26A00] hover:underline hover:text-[#8B5300] dark:text-[#F0C46B] dark:hover:text-[#F5D697]',
184
+ closeClass: 'text-[#AA9B73] hover:bg-[#FFF3D6] hover:text-[#B26A00] dark:text-[#A99668] dark:hover:bg-[#4B3810] dark:hover:text-[#F0C46B]',
100
185
  },
101
186
  error: {
102
- icon: <XCircle size={16} className="shrink-0 text-(--danger)" />,
103
- color: 'border-red-200 dark:border-red-900',
187
+ icon: <XCircle size={14} className="text-[#E01E5A] dark:text-[#e06082]" />,
188
+ titleClass: 'text-[#E01E5A] dark:text-[#e06082]',
189
+ surfaceClass: 'bg-[#FEF6F8] border-[#F8E2E8] dark:bg-[#3A1822] dark:border-[#5E2735]',
190
+ actionClass: 'text-[#C61B50] hover:underline hover:text-[#9E153F] dark:text-[#F08CA8] dark:hover:text-[#F5B0C2]',
191
+ closeClass: 'text-[#B0929C] hover:bg-[#FBE8EE] hover:text-[#E01E5A] dark:text-[#A88E96] dark:hover:bg-[#4A1F2C] dark:hover:text-[#F08CA8]',
104
192
  },
105
193
  }
106
194
 
107
195
  function ToastItem({ toast }: { toast: BaseToast.Root.ToastObject }) {
108
196
  const type = (toast.type ?? 'default') as ToastType
109
- const style = typeStyles[type] ?? typeStyles.default
197
+ const config = typeConfig[type] ?? typeConfig.default
110
198
 
111
199
  return (
112
200
  <BaseToast.Root
113
201
  toast={toast}
202
+ swipeDirection={[]}
114
203
  className={clsx(
115
- 'flex w-full items-start gap-3 rounded-lg border bg-(--bg-primary) px-4 py-3 shadow-lg',
116
- style.color,
117
- 'data-[ending]:animate-[fade-out_150ms_ease-in] data-[starting]:animate-[zoom-in_150ms_ease-out]',
204
+ 'group relative flex w-full items-start gap-2.5 overflow-hidden',
205
+ 'rounded-lg pl-3 pr-3 pt-3 pb-3',
206
+ 'border border-(--border-light)',
207
+ 'shadow-[0_2px_16px_rgba(0,0,0,0.08),0_1px_4px_rgba(0,0,0,0.05)]',
208
+ 'dark:shadow-[0_4px_24px_rgba(0,0,0,0.45)]',
209
+ 'data-[starting]:animate-[toast-slide-in_240ms_cubic-bezier(0.16,1,0.3,1)]',
210
+ 'data-[ending]:animate-[toast-slide-out_180ms_ease-in_forwards]',
211
+ config.surfaceClass,
118
212
  )}
119
213
  >
120
- {style.icon && <div className="pt-0.5">{style.icon}</div>}
214
+ {/* ── Icon ─────────────────────────────────────────── */}
215
+ {config.icon && (
216
+ <div className="flex h-5 shrink-0 items-center">{config.icon}</div>
217
+ )}
121
218
 
122
- <div className="flex flex-1 flex-col gap-0.5 min-w-0">
219
+ {/* ── Content ──────────────────────────────────────── */}
220
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
123
221
  {toast.title && (
124
- <BaseToast.Title className="text-[14px] font-semibold text-(--text-primary) leading-snug">
222
+ <BaseToast.Title
223
+ className={clsx('text-[14px] font-semibold leading-snug', config.titleClass)}
224
+ >
125
225
  {toast.title}
126
226
  </BaseToast.Title>
127
227
  )}
128
228
  {toast.description && (
129
- <BaseToast.Description className="text-[13px] text-(--text-secondary) leading-snug">
229
+ <BaseToast.Description
230
+ className="text-[13px] leading-snug text-(--text-secondary) dark:text-white/65"
231
+ >
130
232
  {toast.description}
131
233
  </BaseToast.Description>
132
234
  )}
@@ -134,15 +236,23 @@ function ToastItem({ toast }: { toast: BaseToast.Root.ToastObject }) {
134
236
  <button
135
237
  {...toast.actionProps}
136
238
  className={clsx(
137
- 'mt-1.5 self-start text-[13px] font-semibold text-(--accent) hover:underline outline-none',
138
- 'focus-visible:ring-1 focus-visible:ring-(--focus-ring) rounded',
239
+ 'mt-1.5 self-start rounded text-[13px] font-semibold outline-none transition-colors',
240
+ config.actionClass,
241
+ 'focus-visible:ring-1 focus-visible:ring-(--focus-ring) dark:focus-visible:ring-white/50',
139
242
  )}
140
243
  />
141
244
  )}
142
245
  </div>
143
246
 
144
- <BaseToast.Close className="shrink-0 rounded p-0.5 text-(--text-muted) hover:text-(--text-primary) hover:bg-(--bg-hover) outline-none focus-visible:ring-1 focus-visible:ring-(--focus-ring) transition-colors">
145
- <X size={14} />
247
+ {/* ── Close button ─────────────────────────────────── */}
248
+ <BaseToast.Close
249
+ className={clsx(
250
+ 'ml-0.5 shrink-0 rounded p-1 outline-none transition-colors',
251
+ config.closeClass,
252
+ 'focus-visible:ring-1 focus-visible:ring-(--focus-ring) dark:focus-visible:ring-white/50',
253
+ )}
254
+ >
255
+ <X size={13} />
146
256
  </BaseToast.Close>
147
257
  </BaseToast.Root>
148
258
  )
@@ -174,7 +174,7 @@ export type { ProgressProps } from './Progress'
174
174
 
175
175
  // Toast Components
176
176
  export { ToastProvider, useToast } from './Toast'
177
- export type { ToastProviderProps, ToastOptions, ToastType } from './Toast'
177
+ export type { ToastProviderProps, ToastOptions, ToastType, ToastPosition } from './Toast'
178
178
 
179
179
  // Loading Component
180
180
  export { Loading } from './Loading'
package/src/index.css CHANGED
@@ -75,6 +75,28 @@
75
75
  }
76
76
  }
77
77
 
78
+ @keyframes toast-slide-in {
79
+ from {
80
+ opacity: 0;
81
+ transform: translateY(10px) scale(0.96);
82
+ }
83
+ to {
84
+ opacity: 1;
85
+ transform: translateY(0) scale(1);
86
+ }
87
+ }
88
+
89
+ @keyframes toast-slide-out {
90
+ from {
91
+ opacity: 1;
92
+ transform: scale(1);
93
+ }
94
+ to {
95
+ opacity: 0;
96
+ transform: scale(0.94);
97
+ }
98
+ }
99
+
78
100
  :root {
79
101
  /* Colors - Slack Brand */
80
102
  --slack-aubergine: #3F0E40;
@@ -1,4 +1,5 @@
1
1
  import { Link } from 'react-router-dom'
2
+ import clsx from 'clsx'
2
3
  import {
3
4
  ArrowLeft,
4
5
  Bell,
@@ -93,6 +94,7 @@ import { Switch } from '../components/Switch'
93
94
  import { Tabs, TabList, Tab, TabPanel } from '../components/Tabs'
94
95
  import { Progress } from '../components/Progress'
95
96
  import { useToast } from '../components/Toast'
97
+ import type { ToastPosition } from '../components/Toast'
96
98
  import { Loading } from '../components/Loading'
97
99
  import { AutoComplete } from '../components/AutoComplete'
98
100
 
@@ -126,7 +128,7 @@ export const ComponentShowcase = () => {
126
128
  const [switchOn, setSwitchOn] = useState(false)
127
129
  const [notificationsOn, setNotificationsOn] = useState(true)
128
130
  const [progressValue, setProgressValue] = useState(60)
129
- const { toast } = useToast()
131
+ const { toast, position: toastPosition, setPosition: setToastPosition } = useToast()
130
132
 
131
133
  // AutoComplete states
132
134
  const [acValue, setAcValue] = useState('')
@@ -1868,94 +1870,152 @@ export const ComponentShowcase = () => {
1868
1870
  <section className="max-w-5xl mx-auto px-8 pb-16 space-y-6">
1869
1871
  <div className="pb-2 border-b border-(--border-light)">
1870
1872
  <h2 className="text-2xl font-bold text-(--text-primary)">Toast</h2>
1871
- <p className="text-(--text-secondary) mt-1">Lightweight notification toasts with five semantic types.</p>
1873
+ <p className="text-(--text-secondary) mt-1">
1874
+ 轻量通知条,支持五种语义类型和六个显示位置。通过{' '}
1875
+ <code className="text-sm bg-(--bg-secondary) px-1.5 py-0.5 rounded border border-(--border-light) font-mono">useToast()</code>{' '}
1876
+ 命令式调用。
1877
+ </p>
1872
1878
  </div>
1879
+
1873
1880
  <div className="grid grid-cols-1 md:grid-cols-2 gap-10">
1881
+ {/* Types */}
1874
1882
  <div className="space-y-4">
1875
- <h3 className="font-semibold text-(--text-secondary)">Types</h3>
1883
+ <h3 className="font-semibold text-(--text-secondary)">类型 Types</h3>
1876
1884
  <div className="flex flex-wrap gap-3">
1877
1885
  <Button
1878
- size="sm"
1879
- onClick={() => toast(
1880
- { title: 'Default notification', description: 'This is a default toast message.' })}
1886
+ size="sm"
1887
+ onClick={() => toast({ title: 'Default notification', description: 'This is a default toast message.' })}
1881
1888
  >
1882
1889
  Default
1883
1890
  </Button>
1884
1891
  <Button
1885
- size="sm"
1886
- onClick={() => toast(
1887
- { type: 'info', title: 'Info', description: 'Your workspace is being updated.' })}
1892
+ size="sm"
1893
+ onClick={() => toast({ type: 'info', title: 'Info', description: 'Your workspace is being updated.' })}
1888
1894
  >
1889
1895
  Info
1890
1896
  </Button>
1891
1897
  <Button
1892
- size="sm"
1893
- variant="primary"
1894
- onClick={() => toast({
1895
- type: 'success',
1896
- title: 'Message sent!',
1897
- description: 'Your message was delivered to #general.',
1898
- })}
1898
+ size="sm"
1899
+ variant="primary"
1900
+ onClick={() => toast({ type: 'success', title: 'Message sent!', description: 'Your message was delivered to #general.' })}
1899
1901
  >
1900
1902
  Success
1901
1903
  </Button>
1902
1904
  <Button
1903
- size="sm"
1904
- onClick={() => toast({
1905
- type: 'warning',
1906
- title: 'Storage almost full',
1907
- description: 'You have used 90% of your storage.',
1908
- })}
1905
+ size="sm"
1906
+ onClick={() => toast({ type: 'warning', title: 'Storage almost full', description: 'You have used 90% of your storage.' })}
1909
1907
  >
1910
1908
  Warning
1911
1909
  </Button>
1912
1910
  <Button
1913
- size="sm"
1914
- variant="danger"
1915
- onClick={() => toast({
1916
- type: 'error',
1917
- title: 'Failed to send',
1918
- description: 'Could not deliver the message. Try again.',
1919
- })}
1911
+ size="sm"
1912
+ variant="danger"
1913
+ onClick={() => toast({ type: 'error', title: 'Failed to send', description: 'Could not deliver the message. Try again.' })}
1920
1914
  >
1921
1915
  Error
1922
1916
  </Button>
1923
1917
  </div>
1924
1918
  </div>
1919
+
1920
+ {/* With action */}
1925
1921
  <div className="space-y-4">
1926
- <h3 className="font-semibold text-(--text-secondary)">With action</h3>
1922
+ <h3 className="font-semibold text-(--text-secondary)">带操作按钮 With action</h3>
1927
1923
  <div className="flex flex-wrap gap-3">
1928
1924
  <Button
1929
- size="sm"
1930
- onClick={() =>
1931
- toast({
1932
- type: 'info',
1933
- title: 'New message in #general',
1934
- description: 'Alice: "Hey everyone, quick update…"',
1935
- action: {
1936
- label: 'View',
1937
- onClick: () => {},
1938
- },
1939
- })
1940
- }
1925
+ size="sm"
1926
+ onClick={() =>
1927
+ toast({
1928
+ type: 'info',
1929
+ title: 'New message in #general',
1930
+ description: 'Alice: "Hey everyone, quick update…"',
1931
+ action: { label: 'View', onClick: () => {} },
1932
+ })
1933
+ }
1941
1934
  >
1942
1935
  Notification with action
1943
1936
  </Button>
1944
1937
  <Button
1945
- size="sm"
1946
- onClick={() =>
1947
- toast({
1948
- type: 'success',
1949
- title: 'Invite sent',
1950
- description: 'bob@example.com will receive an invitation.',
1951
- timeout: 8000,
1952
- })
1953
- }
1938
+ size="sm"
1939
+ onClick={() =>
1940
+ toast({
1941
+ type: 'success',
1942
+ title: 'Invite sent',
1943
+ description: 'bob@example.com will receive an invitation.',
1944
+ timeout: 8000,
1945
+ })
1946
+ }
1954
1947
  >
1955
1948
  Custom timeout (8s)
1956
1949
  </Button>
1957
1950
  </div>
1958
1951
  </div>
1952
+
1953
+ {/* Position picker — spans full width */}
1954
+ <div className="space-y-4 md:col-span-2">
1955
+ <h3 className="font-semibold text-(--text-secondary)">自定义位置 Position</h3>
1956
+ <p className="text-sm text-(--text-secondary)">
1957
+ 可在 <code className="text-xs bg-(--bg-secondary) px-1 py-0.5 rounded border border-(--border-light) font-mono">&lt;ToastProvider position="…"&gt;</code> 设置默认位置,
1958
+ 或运行时通过 <code className="text-xs bg-(--bg-secondary) px-1 py-0.5 rounded border border-(--border-light) font-mono">setPosition()</code> 动态切换。
1959
+ 点击下方格子即可预览对应位置。
1960
+ </p>
1961
+
1962
+ {/* Visual 3×2 position grid */}
1963
+ <div className="relative w-full h-44 rounded-xl border-2 border-dashed border-(--border-light) bg-(--bg-muted) overflow-hidden">
1964
+ {/* 3 cols × 2 rows grid */}
1965
+ <div className="absolute inset-0 grid grid-cols-3 grid-rows-2">
1966
+ {(
1967
+ [
1968
+ ['top-left', '↖', 'top-left'],
1969
+ ['top-center', '↑', 'top-center'],
1970
+ ['top-right', '↗', 'top-right'],
1971
+ ['bottom-left', '↙', 'bottom-left'],
1972
+ ['bottom-center','↓', 'bottom-center'],
1973
+ ['bottom-right', '↘', 'bottom-right'],
1974
+ ] as [ToastPosition, string, string][]
1975
+ ).map(([pos, arrow]) => {
1976
+ const active = toastPosition === pos
1977
+ return (
1978
+ <button
1979
+ key={pos}
1980
+ onClick={() => {
1981
+ setToastPosition(pos)
1982
+ toast({
1983
+ type: 'info',
1984
+ title: pos,
1985
+ description: `Toast 显示在 ${pos}`,
1986
+ timeout: 3000,
1987
+ })
1988
+ }}
1989
+ className={clsx(
1990
+ 'relative flex flex-col items-center justify-center gap-1 transition-all duration-150',
1991
+ 'border border-(--border-light) text-xs font-medium',
1992
+ active
1993
+ ? 'bg-(--accent-action) text-(--accent-contrast) font-semibold shadow-inner'
1994
+ : 'text-(--text-muted) hover:bg-(--bg-hover) hover:text-(--text-primary)',
1995
+ )}
1996
+ >
1997
+ <span className="text-xl leading-none">{arrow}</span>
1998
+ <span className="text-[10px] opacity-80 px-1 text-center leading-tight">
1999
+ {pos.replace('-', '\u00A0')}
2000
+ </span>
2001
+ </button>
2002
+ )
2003
+ })}
2004
+ </div>
2005
+
2006
+ {/* Center label */}
2007
+ <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
2008
+ <span className="text-[11px] text-(--text-muted) select-none px-2 py-1 bg-(--bg-muted)/80 rounded">
2009
+ 点击格子切换位置
2010
+ </span>
2011
+ </div>
2012
+ </div>
2013
+
2014
+ <p className="text-xs text-(--text-muted)">
2015
+ 当前位置:
2016
+ <span className="font-semibold text-(--text-primary) ml-1">{toastPosition}</span>
2017
+ </p>
2018
+ </div>
1959
2019
  </div>
1960
2020
  </section>
1961
2021