panelui-native 0.78.0 → 0.79.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.
- package/lib/module/components/accordion/index.js +13 -1
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/map/index.js +63 -39
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-children.js +54 -0
- package/lib/module/components/map/map-children.js.map +1 -0
- package/lib/module/components/map/maplibre.js +31 -3
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/markdown-editor/index.js +288 -34
- package/lib/module/components/markdown-editor/index.js.map +1 -1
- package/lib/module/components/markdown-editor/markdown-transforms.js +272 -9
- package/lib/module/components/markdown-editor/markdown-transforms.js.map +1 -1
- package/lib/module/components/marquee/index.js +119 -46
- package/lib/module/components/marquee/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +162 -34
- package/lib/module/components/panelside/index.js.map +1 -1
- package/lib/module/components/progress-button/index.js +279 -52
- package/lib/module/components/progress-button/index.js.map +1 -1
- package/lib/module/components/progress-button/progress-button-hold.js +40 -6
- package/lib/module/components/progress-button/progress-button-hold.js.map +1 -1
- package/lib/module/components/theme-selector/index.js +239 -0
- package/lib/module/components/theme-selector/index.js.map +1 -0
- package/lib/module/components/theme-selector/theme-preview.js +320 -0
- package/lib/module/components/theme-selector/theme-preview.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -4
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-children.d.ts +41 -0
- package/lib/typescript/src/components/map/map-children.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +8 -4
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/index.d.ts +64 -5
- package/lib/typescript/src/components/markdown-editor/index.d.ts.map +1 -1
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts +33 -5
- package/lib/typescript/src/components/markdown-editor/markdown-transforms.d.ts.map +1 -1
- package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +79 -4
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/index.d.ts +29 -13
- package/lib/typescript/src/components/progress-button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts +26 -6
- package/lib/typescript/src/components/progress-button/progress-button-hold.d.ts.map +1 -1
- package/lib/typescript/src/components/theme-selector/index.d.ts +107 -0
- package/lib/typescript/src/components/theme-selector/index.d.ts.map +1 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts +32 -0
- package/lib/typescript/src/components/theme-selector/theme-preview.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/accordion/index.tsx +13 -0
- package/src/components/map/index.tsx +63 -36
- package/src/components/map/map-children.ts +66 -0
- package/src/components/map/maplibre.ts +32 -4
- package/src/components/markdown-editor/index.tsx +413 -35
- package/src/components/markdown-editor/markdown-transforms.ts +274 -10
- package/src/components/marquee/index.tsx +128 -55
- package/src/components/panelside/index.tsx +179 -40
- package/src/components/progress-button/index.tsx +264 -45
- package/src/components/progress-button/progress-button-hold.ts +37 -6
- package/src/components/theme-selector/index.tsx +281 -0
- package/src/components/theme-selector/theme-preview.tsx +205 -0
- package/src/index.ts +15 -0
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThemeSelector — light, dark, or whatever the device is set to.
|
|
3
|
+
*
|
|
4
|
+
* Three miniatures of a screen, the chosen one ringed. A row of words would
|
|
5
|
+
* take a third of the space and say the same thing, but "System" is the option
|
|
6
|
+
* people hesitate over, and a picture of a screen split down the middle
|
|
7
|
+
* explains it in less time than a sentence does.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <ThemeSelector label="Choose a theme" />
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* ## It reads the theme rather than remembering it
|
|
14
|
+
*
|
|
15
|
+
* There is already one answer to "which theme is this" — Uniwind's — so the
|
|
16
|
+
* selector asks for it instead of keeping a copy that can disagree. Change the
|
|
17
|
+
* theme anywhere else in the app and the ring moves. Pass `value` to show
|
|
18
|
+
* something else, for a settings screen that stages a choice before applying
|
|
19
|
+
* it.
|
|
20
|
+
*
|
|
21
|
+
* ## Light and dark stay inside the family
|
|
22
|
+
*
|
|
23
|
+
* A reader in Moon who picks Light gets Moon's light theme, not the default
|
|
24
|
+
* one. System is the exception, and cannot be otherwise: the device knows only
|
|
25
|
+
* light and dark, so following it means following those two.
|
|
26
|
+
*/
|
|
27
|
+
import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react';
|
|
28
|
+
import { Pressable, View, type PressableProps, type ViewProps } from 'react-native';
|
|
29
|
+
import { tv } from 'tailwind-variants';
|
|
30
|
+
import { useUniwind } from 'uniwind';
|
|
31
|
+
import { Text, textChildren } from '../../primitives/text';
|
|
32
|
+
import { useTheme, useThemeMode } from '../../theme/use-theme';
|
|
33
|
+
import { ThemePreview, type ThemePreviewVariant } from './theme-preview';
|
|
34
|
+
|
|
35
|
+
export type { ThemePreviewVariant } from './theme-preview';
|
|
36
|
+
|
|
37
|
+
/** What the selector chooses between. */
|
|
38
|
+
export type ThemeSelection = 'system' | 'light' | 'dark';
|
|
39
|
+
|
|
40
|
+
/** How big the miniatures are drawn. */
|
|
41
|
+
export type ThemeSelectorSize = 'sm' | 'md';
|
|
42
|
+
|
|
43
|
+
/** The three, in the order a selector draws them when it is not told. */
|
|
44
|
+
const OPTIONS: readonly ThemeSelection[] = ['system', 'light', 'dark'];
|
|
45
|
+
|
|
46
|
+
/** What each one is called, when the caller does not say. */
|
|
47
|
+
const LABELS: Record<ThemeSelection, string> = {
|
|
48
|
+
system: 'System',
|
|
49
|
+
light: 'Light',
|
|
50
|
+
dark: 'Dark',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** How wide the miniature is drawn, per size. */
|
|
54
|
+
const PREVIEW_WIDTH: Record<ThemeSelectorSize, number> = { sm: 72, md: 88 };
|
|
55
|
+
|
|
56
|
+
const themeSelectorVariants = tv({
|
|
57
|
+
slots: {
|
|
58
|
+
root: 'gap-3',
|
|
59
|
+
legend: 'font-medium',
|
|
60
|
+
options: 'flex-row flex-wrap items-start gap-4',
|
|
61
|
+
option: 'items-center gap-2',
|
|
62
|
+
/*
|
|
63
|
+
* The ring lives on a box around the artwork rather than on the artwork
|
|
64
|
+
* itself. React Native has no outline, so a border on the preview would
|
|
65
|
+
* take a strip out of the drawing every time the option was chosen — the
|
|
66
|
+
* miniature would change size as you picked it.
|
|
67
|
+
*/
|
|
68
|
+
frame: 'overflow-hidden rounded-xl border-2 p-1',
|
|
69
|
+
/*
|
|
70
|
+
* The artwork's own corners, rounded to sit concentrically inside the
|
|
71
|
+
* frame: the frame's radius less the padding between them. A square
|
|
72
|
+
* miniature inside a rounded ring reads as a screenshot somebody pasted in
|
|
73
|
+
* — the two edges have to curve together or neither should.
|
|
74
|
+
*/
|
|
75
|
+
preview: 'overflow-hidden rounded-lg',
|
|
76
|
+
label: 'text-center',
|
|
77
|
+
},
|
|
78
|
+
variants: {
|
|
79
|
+
selected: {
|
|
80
|
+
true: { frame: 'border-primary', label: 'text-foreground' },
|
|
81
|
+
false: { frame: 'border-transparent', label: 'text-muted-foreground' },
|
|
82
|
+
},
|
|
83
|
+
disabled: {
|
|
84
|
+
true: { root: 'opacity-40' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
defaultVariants: { selected: false },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
interface ThemeSelectorContextValue {
|
|
91
|
+
value: ThemeSelection;
|
|
92
|
+
select: (value: ThemeSelection) => void;
|
|
93
|
+
variant: ThemePreviewVariant;
|
|
94
|
+
size: ThemeSelectorSize;
|
|
95
|
+
disabled: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ThemeSelectorContext = createContext<ThemeSelectorContextValue | null>(null);
|
|
99
|
+
|
|
100
|
+
function useThemeSelector(part: string): ThemeSelectorContextValue {
|
|
101
|
+
const value = useContext(ThemeSelectorContext);
|
|
102
|
+
if (!value) throw new Error(`${part} must be used inside a <ThemeSelector>.`);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Which of the three the app is on, read from the theme itself.
|
|
108
|
+
*
|
|
109
|
+
* `hasAdaptiveThemes` is the part that makes this possible: it is true exactly
|
|
110
|
+
* while the theme is following the device, which is the one thing the theme's
|
|
111
|
+
* own name cannot tell you — `system` resolves to `light` or `dark` the moment
|
|
112
|
+
* it is applied, and the two are indistinguishable afterwards.
|
|
113
|
+
*/
|
|
114
|
+
export function useThemeSelection(): ThemeSelection {
|
|
115
|
+
const { hasAdaptiveThemes } = useUniwind();
|
|
116
|
+
const { mode } = useThemeMode();
|
|
117
|
+
return hasAdaptiveThemes ? 'system' : mode;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ThemeSelectorProps extends Omit<ViewProps, 'children'> {
|
|
121
|
+
className?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Show this as chosen instead of whatever the app is actually on. For a
|
|
124
|
+
* settings screen that stages a choice before applying it; left unset, the
|
|
125
|
+
* selector reads the live theme.
|
|
126
|
+
*/
|
|
127
|
+
value?: ThemeSelection;
|
|
128
|
+
/** Fires with the option pressed, before the theme changes. */
|
|
129
|
+
onValueChange?: (value: ThemeSelection) => void;
|
|
130
|
+
/**
|
|
131
|
+
* Apply the choice. On by default — a theme selector that does not select a
|
|
132
|
+
* theme is a radio group. Turn it off to store the choice and apply it
|
|
133
|
+
* yourself, which is what an app that persists the preference wants.
|
|
134
|
+
*/
|
|
135
|
+
applyTheme?: boolean;
|
|
136
|
+
/** The heading above the row. Left out, there is none. */
|
|
137
|
+
label?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Which miniature is drawn. `window` is an app screen with a panel on it;
|
|
140
|
+
* `card` is a framed card with an accent, cut on the diagonal for system.
|
|
141
|
+
*/
|
|
142
|
+
variant?: ThemePreviewVariant;
|
|
143
|
+
/**
|
|
144
|
+
* How wide the miniatures are drawn. `sm` for a settings row that has other
|
|
145
|
+
* things on it; `md` when choosing the theme is what the screen is for.
|
|
146
|
+
*/
|
|
147
|
+
size?: ThemeSelectorSize;
|
|
148
|
+
/** Stop the row being pressed, and dim it to say so. */
|
|
149
|
+
disabled?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The options, in the order you want them. Left out, the selector draws
|
|
152
|
+
* system, light and dark in that order — which is the whole component.
|
|
153
|
+
*/
|
|
154
|
+
children?: ReactNode;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ThemeSelectorRoot({
|
|
158
|
+
className,
|
|
159
|
+
value,
|
|
160
|
+
onValueChange,
|
|
161
|
+
applyTheme = true,
|
|
162
|
+
label,
|
|
163
|
+
variant = 'window',
|
|
164
|
+
size = 'md',
|
|
165
|
+
disabled = false,
|
|
166
|
+
children,
|
|
167
|
+
...props
|
|
168
|
+
}: ThemeSelectorProps) {
|
|
169
|
+
const slots = themeSelectorVariants({ disabled });
|
|
170
|
+
const { setTheme } = useTheme();
|
|
171
|
+
const { setMode } = useThemeMode();
|
|
172
|
+
const live = useThemeSelection();
|
|
173
|
+
const current = value ?? live;
|
|
174
|
+
|
|
175
|
+
const select = useCallback(
|
|
176
|
+
(next: ThemeSelection) => {
|
|
177
|
+
onValueChange?.(next);
|
|
178
|
+
if (!applyTheme) return;
|
|
179
|
+
// `setMode` rather than `setTheme`, so a reader in a named family stays
|
|
180
|
+
// in it. Following the device is the one choice that cannot: the device
|
|
181
|
+
// has no opinion beyond light and dark.
|
|
182
|
+
if (next === 'system') setTheme('system');
|
|
183
|
+
else setMode(next);
|
|
184
|
+
},
|
|
185
|
+
[applyTheme, onValueChange, setTheme, setMode]
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const context = useMemo<ThemeSelectorContextValue>(
|
|
189
|
+
() => ({ value: current, select, variant, size, disabled }),
|
|
190
|
+
[current, select, variant, size, disabled]
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<ThemeSelectorContext.Provider value={context}>
|
|
195
|
+
<View
|
|
196
|
+
accessibilityRole="radiogroup"
|
|
197
|
+
accessibilityLabel={label}
|
|
198
|
+
{...props}
|
|
199
|
+
className={slots.root({ className })}
|
|
200
|
+
>
|
|
201
|
+
{label ? (
|
|
202
|
+
<Text size="sm" className={slots.legend()}>
|
|
203
|
+
{label}
|
|
204
|
+
</Text>
|
|
205
|
+
) : null}
|
|
206
|
+
<View className={slots.options()}>
|
|
207
|
+
{children
|
|
208
|
+
? textChildren(children)
|
|
209
|
+
: OPTIONS.map((option) => (
|
|
210
|
+
<ThemeSelectorOption key={option} value={option} />
|
|
211
|
+
))}
|
|
212
|
+
</View>
|
|
213
|
+
</View>
|
|
214
|
+
</ThemeSelectorContext.Provider>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ThemeSelectorOptionProps
|
|
219
|
+
extends Omit<PressableProps, 'children' | 'onPress'> {
|
|
220
|
+
className?: string;
|
|
221
|
+
/** Which of the three this option chooses. */
|
|
222
|
+
value: ThemeSelection;
|
|
223
|
+
/** What it is called under the miniature. Defaults to System, Light or Dark. */
|
|
224
|
+
label?: string;
|
|
225
|
+
/** Replaces the drawn miniature. */
|
|
226
|
+
children?: ReactNode;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* One of the three.
|
|
231
|
+
*
|
|
232
|
+
* The whole thing is the target — the miniature and the word under it — because
|
|
233
|
+
* at this size the word is the easier of the two to hit and pressing it should
|
|
234
|
+
* not miss.
|
|
235
|
+
*/
|
|
236
|
+
function ThemeSelectorOption({
|
|
237
|
+
className,
|
|
238
|
+
value,
|
|
239
|
+
label,
|
|
240
|
+
children,
|
|
241
|
+
...props
|
|
242
|
+
}: ThemeSelectorOptionProps) {
|
|
243
|
+
const context = useThemeSelector('ThemeSelector.Option');
|
|
244
|
+
const selected = context.value === value;
|
|
245
|
+
const slots = themeSelectorVariants({ selected, disabled: context.disabled });
|
|
246
|
+
const name = label ?? LABELS[value];
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<Pressable
|
|
250
|
+
accessibilityRole="radio"
|
|
251
|
+
accessibilityLabel={name}
|
|
252
|
+
accessibilityState={{ selected, disabled: context.disabled }}
|
|
253
|
+
disabled={context.disabled}
|
|
254
|
+
{...props}
|
|
255
|
+
onPress={() => context.select(value)}
|
|
256
|
+
className={slots.option({ className })}
|
|
257
|
+
>
|
|
258
|
+
<View className={slots.frame()}>
|
|
259
|
+
<View className={slots.preview()}>
|
|
260
|
+
{children ?? (
|
|
261
|
+
<ThemePreview
|
|
262
|
+
mode={value}
|
|
263
|
+
variant={context.variant}
|
|
264
|
+
width={PREVIEW_WIDTH[context.size]}
|
|
265
|
+
/>
|
|
266
|
+
)}
|
|
267
|
+
</View>
|
|
268
|
+
</View>
|
|
269
|
+
<Text size="sm" className={slots.label()}>
|
|
270
|
+
{name}
|
|
271
|
+
</Text>
|
|
272
|
+
</Pressable>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
ThemeSelectorRoot.displayName = 'ThemeSelector';
|
|
277
|
+
ThemeSelectorOption.displayName = 'ThemeSelector.Option';
|
|
278
|
+
|
|
279
|
+
export const ThemeSelector = Object.assign(ThemeSelectorRoot, {
|
|
280
|
+
Option: ThemeSelectorOption,
|
|
281
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three miniatures a theme selector is made of.
|
|
3
|
+
*
|
|
4
|
+
* Drawn from fixed neutrals rather than from theme tokens, which is the one
|
|
5
|
+
* thing about this file that is not obvious. A preview built out of the active
|
|
6
|
+
* theme's colours shows the reader the theme they already have, three times —
|
|
7
|
+
* the light option has to look light while the app around it is dark, or it is
|
|
8
|
+
* not a preview of anything.
|
|
9
|
+
*
|
|
10
|
+
* The greys are the ones a screenshot of an app would actually have: a ground,
|
|
11
|
+
* a surface sitting on it, a mark, and the lines of text. Two sets, and the
|
|
12
|
+
* system option draws both.
|
|
13
|
+
*/
|
|
14
|
+
import { useId, type ReactNode } from 'react';
|
|
15
|
+
import Svg, { Circle, ClipPath, Defs, G, Path, Rect } from 'react-native-svg';
|
|
16
|
+
|
|
17
|
+
/** Which of the three a preview is drawing. */
|
|
18
|
+
export type ThemePreviewMode = 'system' | 'light' | 'dark';
|
|
19
|
+
|
|
20
|
+
/** How the miniature is drawn. */
|
|
21
|
+
export type ThemePreviewVariant = 'window' | 'card';
|
|
22
|
+
|
|
23
|
+
interface Palette {
|
|
24
|
+
/** Behind everything — the desktop, the page, the space around the app. */
|
|
25
|
+
ground: string;
|
|
26
|
+
/** The app's own surface. */
|
|
27
|
+
surface: string;
|
|
28
|
+
/** A shape on it: an avatar, an image, a control. */
|
|
29
|
+
mark: string;
|
|
30
|
+
/** Lines of text. */
|
|
31
|
+
line: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const PALETTE: Record<'light' | 'dark', Palette> = {
|
|
35
|
+
light: { ground: '#e5e5e5', surface: '#ffffff', mark: '#d4d4d4', line: '#e5e5e5' },
|
|
36
|
+
dark: { ground: '#171717', surface: '#262626', mark: '#525252', line: '#404040' },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The one colour that is not a grey.
|
|
41
|
+
*
|
|
42
|
+
* Fixed for the same reason the greys are, and warm rather than blue: it has to
|
|
43
|
+
* read as "a colour in the app" on both grounds without looking like the ring
|
|
44
|
+
* that marks the chosen option.
|
|
45
|
+
*/
|
|
46
|
+
const ACCENT = '#c1704f';
|
|
47
|
+
|
|
48
|
+
/** Everything is drawn in this box and scaled by the container. */
|
|
49
|
+
const VIEW_BOX = { width: 88, height: 70 };
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The lower-right half of the box, cut from the top-right corner to the
|
|
53
|
+
* bottom-left one. Both text lines sit above it and the accent below it, which
|
|
54
|
+
* is what makes one card show a light screen and a dark one at the same time.
|
|
55
|
+
*/
|
|
56
|
+
const DIAGONAL = `M${VIEW_BOX.width} 0V${VIEW_BOX.height}H0Z`;
|
|
57
|
+
|
|
58
|
+
/* -------------------------------------------------------------------------- *
|
|
59
|
+
* window — an app screen, with the panel inset from the leading edge
|
|
60
|
+
* -------------------------------------------------------------------------- */
|
|
61
|
+
|
|
62
|
+
function WindowHalf({ palette, side }: { palette: Palette; side: 'left' | 'right' | 'full' }) {
|
|
63
|
+
if (side === 'full') {
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
<Rect x={0} y={0} width={88} height={70} fill={palette.ground} />
|
|
67
|
+
<Path d="M10 12a4 4 0 0 1 4-4h74v62H10V12Z" fill={palette.surface} />
|
|
68
|
+
<Circle cx={28} cy={26} r={8} fill={palette.mark} />
|
|
69
|
+
<Rect x={20} y={42} width={58} height={4} rx={2} fill={palette.line} />
|
|
70
|
+
<Rect x={20} y={49} width={58} height={4} rx={2} fill={palette.line} />
|
|
71
|
+
<Rect x={20} y={56} width={29} height={4} rx={2} fill={palette.line} />
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* Half a screen each, and the halves are not mirror images: the panel starts
|
|
78
|
+
* at the same inset on the left and runs to the edge on the right, so the
|
|
79
|
+
* seam falls where the two app surfaces meet rather than down the middle of
|
|
80
|
+
* one of them.
|
|
81
|
+
*/
|
|
82
|
+
const x = side === 'left' ? 0 : 44;
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<Rect x={x} y={0} width={44} height={70} fill={palette.ground} />
|
|
86
|
+
{side === 'left' ? (
|
|
87
|
+
<>
|
|
88
|
+
<Path d="M10 12a4 4 0 0 1 4-4h30v62H10V12Z" fill={palette.surface} />
|
|
89
|
+
<Circle cx={28} cy={26} r={8} fill={palette.mark} />
|
|
90
|
+
<Rect x={20} y={42} width={24} height={4} rx={2} fill={palette.line} />
|
|
91
|
+
<Rect x={20} y={49} width={24} height={4} rx={2} fill={palette.line} />
|
|
92
|
+
<Rect x={20} y={56} width={24} height={4} rx={2} fill={palette.line} />
|
|
93
|
+
</>
|
|
94
|
+
) : (
|
|
95
|
+
<>
|
|
96
|
+
<Path d="M54 12a4 4 0 0 1 4-4h30v62H54V12Z" fill={palette.surface} />
|
|
97
|
+
<Circle cx={72} cy={26} r={8} fill={palette.mark} />
|
|
98
|
+
<Rect x={64} y={42} width={24} height={4} rx={2} fill={palette.line} />
|
|
99
|
+
<Rect x={64} y={49} width={24} height={4} rx={2} fill={palette.line} />
|
|
100
|
+
<Rect x={64} y={56} width={24} height={4} rx={2} fill={palette.line} />
|
|
101
|
+
</>
|
|
102
|
+
)}
|
|
103
|
+
</>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* -------------------------------------------------------------------------- *
|
|
108
|
+
* card — a framed card, with an accent on it
|
|
109
|
+
* -------------------------------------------------------------------------- */
|
|
110
|
+
|
|
111
|
+
/*
|
|
112
|
+
* The card fills its box rather than floating inside it.
|
|
113
|
+
*
|
|
114
|
+
* It used to carry a margin of its own, which then sat on top of the padding
|
|
115
|
+
* between the artwork and the ring around it — two gaps doing one job, and the
|
|
116
|
+
* bigger the miniature the further apart they held the two edges.
|
|
117
|
+
*/
|
|
118
|
+
const CARD_FRAME = { x: 0, y: 0, width: 88, height: 70, rx: 16 } as const;
|
|
119
|
+
const CARD_SURFACE = { x: 8, y: 8, width: 72, height: 54, rx: 10 } as const;
|
|
120
|
+
|
|
121
|
+
function CardBody({ palette }: { palette: Palette }) {
|
|
122
|
+
return (
|
|
123
|
+
<>
|
|
124
|
+
<Rect {...CARD_FRAME} fill={palette.ground} />
|
|
125
|
+
<Rect {...CARD_SURFACE} fill={palette.surface} />
|
|
126
|
+
<Rect x={17} y={18} width={34} height={4} rx={2} fill={palette.line} />
|
|
127
|
+
<Rect x={17} y={26} width={23} height={4} rx={2} fill={palette.line} />
|
|
128
|
+
</>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface ThemePreviewProps {
|
|
133
|
+
mode: ThemePreviewMode;
|
|
134
|
+
variant?: ThemePreviewVariant;
|
|
135
|
+
width: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* One miniature.
|
|
140
|
+
*
|
|
141
|
+
* `width` rather than a scale factor: the option's box is what decides the
|
|
142
|
+
* size, and everything inside is drawn against a fixed box and scaled by the
|
|
143
|
+
* viewBox — so the geometry above never has to know how big it ended up.
|
|
144
|
+
*/
|
|
145
|
+
export function ThemePreview({ mode, variant = 'window', width }: ThemePreviewProps): ReactNode {
|
|
146
|
+
const height = Math.round((width / VIEW_BOX.width) * VIEW_BOX.height);
|
|
147
|
+
/*
|
|
148
|
+
* `useId` returns something with colons in it, which is not a name an SVG
|
|
149
|
+
* fragment reference accepts — a clip that silently fails to resolve draws
|
|
150
|
+
* nothing at all, and on a shape covering half the artwork that is the whole
|
|
151
|
+
* option missing.
|
|
152
|
+
*/
|
|
153
|
+
const clip = `panelui-theme-${useId().replace(/[^a-zA-Z0-9-]/g, '')}`;
|
|
154
|
+
|
|
155
|
+
if (variant === 'card') {
|
|
156
|
+
return (
|
|
157
|
+
<Svg width={width} height={height} viewBox="0 0 88 70">
|
|
158
|
+
<CardBody palette={PALETTE[mode === 'dark' ? 'dark' : 'light']} />
|
|
159
|
+
|
|
160
|
+
{mode === 'system' ? (
|
|
161
|
+
<>
|
|
162
|
+
{/*
|
|
163
|
+
The other half, cut corner to corner.
|
|
164
|
+
|
|
165
|
+
The same triangle twice, clipped once to the frame and once to
|
|
166
|
+
the card inside it, rather than one shape tracing where the
|
|
167
|
+
diagonal crosses both. Two clips is less arithmetic than one path
|
|
168
|
+
and stays right if either radius changes; an unclipped triangle
|
|
169
|
+
squares off the two corners the diagonal runs into.
|
|
170
|
+
*/}
|
|
171
|
+
<Defs>
|
|
172
|
+
<ClipPath id={`${clip}-frame`}>
|
|
173
|
+
<Rect {...CARD_FRAME} />
|
|
174
|
+
</ClipPath>
|
|
175
|
+
<ClipPath id={`${clip}-card`}>
|
|
176
|
+
<Rect {...CARD_SURFACE} />
|
|
177
|
+
</ClipPath>
|
|
178
|
+
</Defs>
|
|
179
|
+
<G clipPath={`url(#${clip}-frame)`}>
|
|
180
|
+
<Path d={DIAGONAL} fill={PALETTE.dark.ground} />
|
|
181
|
+
</G>
|
|
182
|
+
<G clipPath={`url(#${clip}-card)`}>
|
|
183
|
+
<Path d={DIAGONAL} fill={PALETTE.dark.surface} />
|
|
184
|
+
</G>
|
|
185
|
+
</>
|
|
186
|
+
) : null}
|
|
187
|
+
|
|
188
|
+
<Circle cx={64} cy={47} r={8} fill={ACCENT} />
|
|
189
|
+
</Svg>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<Svg width={width} height={height} viewBox="0 0 88 70">
|
|
195
|
+
{mode === 'system' ? (
|
|
196
|
+
<>
|
|
197
|
+
<WindowHalf palette={PALETTE.light} side="left" />
|
|
198
|
+
<WindowHalf palette={PALETTE.dark} side="right" />
|
|
199
|
+
</>
|
|
200
|
+
) : (
|
|
201
|
+
<WindowHalf palette={PALETTE[mode]} side="full" />
|
|
202
|
+
)}
|
|
203
|
+
</Svg>
|
|
204
|
+
);
|
|
205
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -538,6 +538,9 @@ export {
|
|
|
538
538
|
type PanelsideGroupProps,
|
|
539
539
|
type PanelsideGroupLabelProps,
|
|
540
540
|
type PanelsideItemProps,
|
|
541
|
+
type PanelsideItemIconProps,
|
|
542
|
+
type PanelsideItemLabelProps,
|
|
543
|
+
type PanelsideItemBadgeProps,
|
|
541
544
|
type PanelsideActionProps,
|
|
542
545
|
type PanelsideFooterProps,
|
|
543
546
|
type PanelsideCtaProps,
|
|
@@ -658,6 +661,15 @@ export {
|
|
|
658
661
|
type ChartPoint,
|
|
659
662
|
} from './components/plot';
|
|
660
663
|
export { Textarea, type TextareaProps } from './components/textarea';
|
|
664
|
+
export {
|
|
665
|
+
ThemeSelector,
|
|
666
|
+
useThemeSelection,
|
|
667
|
+
type ThemeSelectorProps,
|
|
668
|
+
type ThemeSelectorOptionProps,
|
|
669
|
+
type ThemeSelection,
|
|
670
|
+
type ThemeSelectorSize,
|
|
671
|
+
type ThemePreviewVariant,
|
|
672
|
+
} from './components/theme-selector';
|
|
661
673
|
export {
|
|
662
674
|
TagInput,
|
|
663
675
|
type TagInputProps,
|
|
@@ -730,8 +742,10 @@ export {
|
|
|
730
742
|
type MarkdownEditorToolbarProps,
|
|
731
743
|
type MarkdownEditorInputProps,
|
|
732
744
|
type MarkdownEditorPreviewProps,
|
|
745
|
+
type MarkdownEditorHandle,
|
|
733
746
|
type MarkdownEditorMode,
|
|
734
747
|
type MarkdownEditorAction,
|
|
748
|
+
type MarkdownEditorToolbarVariant,
|
|
735
749
|
type EditorSelection,
|
|
736
750
|
type EditResult,
|
|
737
751
|
} from './components/markdown-editor';
|
|
@@ -849,6 +863,7 @@ export {
|
|
|
849
863
|
useProgressButton,
|
|
850
864
|
type ProgressButtonProps,
|
|
851
865
|
type ProgressButtonLabelProps,
|
|
866
|
+
type ProgressButtonDoneProps,
|
|
852
867
|
type ProgressButtonVariant,
|
|
853
868
|
type ProgressButtonSize,
|
|
854
869
|
} from './components/progress-button';
|