@djangocfg/widget-visual 0.1.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/LICENSE +21 -0
- package/README.md +41 -0
- package/package.json +106 -0
- package/src/design/ColorPalette/ColorPalette.tsx +129 -0
- package/src/design/ColorPalette/README.md +34 -0
- package/src/design/ColorPalette/color-palette.stories.tsx +95 -0
- package/src/design/ColorPalette/components/Swatch.tsx +102 -0
- package/src/design/ColorPalette/hooks/useCopyToClipboard.ts +41 -0
- package/src/design/ColorPalette/index.ts +12 -0
- package/src/design/ColorPalette/lazy.tsx +21 -0
- package/src/design/ColorPalette/types.ts +63 -0
- package/src/design/ColorPalette/utils.ts +114 -0
- package/src/design/ColorPicker/ColorPicker.tsx +257 -0
- package/src/design/ColorPicker/color-picker.stories.tsx +113 -0
- package/src/design/ColorPicker/context/ColorPickerContext.tsx +28 -0
- package/src/design/ColorPicker/context/ColorPickerStore.tsx +166 -0
- package/src/design/ColorPicker/context/index.ts +2 -0
- package/src/design/ColorPicker/index.ts +15 -0
- package/src/design/ColorPicker/lazy.tsx +24 -0
- package/src/design/ColorPicker/lib/color-utils.ts +323 -0
- package/src/design/ColorPicker/parts/ColorPickerAlphaSlider.tsx +72 -0
- package/src/design/ColorPicker/parts/ColorPickerArea.tsx +155 -0
- package/src/design/ColorPicker/parts/ColorPickerEyeDropper.tsx +73 -0
- package/src/design/ColorPicker/parts/ColorPickerFormatSelect.tsx +64 -0
- package/src/design/ColorPicker/parts/ColorPickerHueSlider.tsx +64 -0
- package/src/design/ColorPicker/parts/ColorPickerInput.tsx +512 -0
- package/src/design/ColorPicker/parts/ColorPickerSwatch.tsx +63 -0
- package/src/design/ColorPicker/parts/index.ts +7 -0
- package/src/design/ColorPicker/types.ts +77 -0
- package/src/gallery/components/Gallery.tsx +182 -0
- package/src/gallery/components/compact/GalleryCompact.tsx +396 -0
- package/src/gallery/components/compact/index.ts +1 -0
- package/src/gallery/components/index.ts +21 -0
- package/src/gallery/components/lightbox/GalleryLightbox.tsx +426 -0
- package/src/gallery/components/lightbox/index.ts +1 -0
- package/src/gallery/components/media/GalleryImage.tsx +105 -0
- package/src/gallery/components/media/GalleryMedia.tsx +70 -0
- package/src/gallery/components/media/GalleryVideo.tsx +241 -0
- package/src/gallery/components/media/index.ts +3 -0
- package/src/gallery/components/preview/GalleryCarousel.tsx +374 -0
- package/src/gallery/components/preview/GalleryGrid.tsx +519 -0
- package/src/gallery/components/preview/index.ts +2 -0
- package/src/gallery/components/shared/ImageSpinner.tsx +37 -0
- package/src/gallery/components/shared/index.ts +1 -0
- package/src/gallery/components/thumbnails/GalleryThumbnails.tsx +198 -0
- package/src/gallery/components/thumbnails/GalleryThumbnailsVirtual.tsx +164 -0
- package/src/gallery/components/thumbnails/index.ts +2 -0
- package/src/gallery/gallery.stories.tsx +182 -0
- package/src/gallery/hooks/index.ts +23 -0
- package/src/gallery/hooks/useGallery.ts +138 -0
- package/src/gallery/hooks/useImageDimensions.ts +224 -0
- package/src/gallery/hooks/usePinchZoom.ts +239 -0
- package/src/gallery/hooks/usePreloadImages.ts +119 -0
- package/src/gallery/hooks/useSwipe.ts +87 -0
- package/src/gallery/hooks/useVirtualList.ts +129 -0
- package/src/gallery/hooks/useZoom.ts +321 -0
- package/src/gallery/index.ts +66 -0
- package/src/gallery/thumbnails.stories.tsx +127 -0
- package/src/gallery/types.ts +185 -0
- package/src/gallery/utils/imageAnalysis.ts +52 -0
- package/src/gallery/utils/index.ts +16 -0
- package/src/gallery/utils/normalizeUrl.ts +31 -0
- package/src/index.ts +18 -0
- package/src/lottie/LottiePlayer.client.tsx +319 -0
- package/src/lottie/index.tsx +66 -0
- package/src/lottie/lazy.tsx +65 -0
- package/src/lottie/lottie-player.stories.tsx +173 -0
- package/src/lottie/types.ts +138 -0
- package/src/lottie/useLottie.ts +187 -0
- package/src/lottie/usePrefersReducedMotion.ts +46 -0
- package/src/marquee/Marquee.tsx +637 -0
- package/src/marquee/index.ts +7 -0
- package/src/marquee/lazy.tsx +14 -0
- package/src/marquee/marquee.stories.tsx +118 -0
- package/src/marquee/types.ts +40 -0
- package/src/qrcode/QRCode.tsx +468 -0
- package/src/qrcode/index.ts +10 -0
- package/src/qrcode/lazy.tsx +19 -0
- package/src/qrcode/qr-code.stories.tsx +102 -0
- package/src/qrcode/types.ts +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Reforms.ai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @djangocfg/widget-visual
|
|
2
|
+
|
|
3
|
+
Presentation pieces: colour, motion, a code symbol, and two ways to show media
|
|
4
|
+
that are not a viewer.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @djangocfg/widget-visual @djangocfg/ui-core @djangocfg/i18n
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
| Subpath | What |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `./color-picker` | a colour picker |
|
|
15
|
+
| `./color-palette` | a palette built from one seed |
|
|
16
|
+
| `./marquee` | a continuous scroll |
|
|
17
|
+
| `./qr-code` | a QR code; the encoder is a dynamic import |
|
|
18
|
+
| `./gallery` | an image grid, carousel, thumbnails and lightbox |
|
|
19
|
+
| `./lottie-player` | a Lottie animation |
|
|
20
|
+
|
|
21
|
+
## What is NOT here
|
|
22
|
+
|
|
23
|
+
This is the residue of ui-tools' `visual/` and `media/` folders after the parts
|
|
24
|
+
with a real home left, and knowing where they went is half the package:
|
|
25
|
+
|
|
26
|
+
| Looking for | It is in |
|
|
27
|
+
|---|---|
|
|
28
|
+
| an image viewer with zoom and pan | `@djangocfg/widget-media` |
|
|
29
|
+
| an audio or video player | `@djangocfg/widget-player` |
|
|
30
|
+
| a chart or an indicator | `@djangocfg/widget-charts` |
|
|
31
|
+
| a dialog, tour or scroll spy | `@djangocfg/widget-overlay` |
|
|
32
|
+
| a file icon | `@djangocfg/widget-data` |
|
|
33
|
+
|
|
34
|
+
What remains genuinely belongs together: each puts something on screen for its
|
|
35
|
+
own sake, and none owns state a host has to drive.
|
|
36
|
+
|
|
37
|
+
## Verify
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm --filter @djangocfg/widget-visual check-types test
|
|
41
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@djangocfg/widget-visual",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Presentation pieces: a colour picker and palette, a marquee, a QR code, an image gallery and a Lottie player",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"color-picker",
|
|
7
|
+
"palette",
|
|
8
|
+
"marquee",
|
|
9
|
+
"qrcode",
|
|
10
|
+
"gallery",
|
|
11
|
+
"lottie",
|
|
12
|
+
"react",
|
|
13
|
+
"typescript"
|
|
14
|
+
],
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "DjangoCFG",
|
|
17
|
+
"url": "https://djangocfg.com"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://djangocfg.com",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/markolofsen/django-cfg.git",
|
|
23
|
+
"directory": "widgets/visual"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "./src/index.ts",
|
|
28
|
+
"types": "./src/index.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./src/index.ts",
|
|
32
|
+
"default": "./src/index.ts"
|
|
33
|
+
},
|
|
34
|
+
"./color-picker": {
|
|
35
|
+
"types": "./src/design/ColorPicker/index.ts",
|
|
36
|
+
"default": "./src/design/ColorPicker/index.ts"
|
|
37
|
+
},
|
|
38
|
+
"./color-palette": {
|
|
39
|
+
"types": "./src/design/ColorPalette/index.ts",
|
|
40
|
+
"default": "./src/design/ColorPalette/index.ts"
|
|
41
|
+
},
|
|
42
|
+
"./marquee": {
|
|
43
|
+
"types": "./src/marquee/index.ts",
|
|
44
|
+
"default": "./src/marquee/index.ts"
|
|
45
|
+
},
|
|
46
|
+
"./qr-code": {
|
|
47
|
+
"types": "./src/qrcode/index.ts",
|
|
48
|
+
"default": "./src/qrcode/index.ts"
|
|
49
|
+
},
|
|
50
|
+
"./gallery": {
|
|
51
|
+
"types": "./src/gallery/index.ts",
|
|
52
|
+
"default": "./src/gallery/index.ts"
|
|
53
|
+
},
|
|
54
|
+
"./lottie-player": {
|
|
55
|
+
"types": "./src/lottie/index.ts",
|
|
56
|
+
"default": "./src/lottie/index.ts"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"files": [
|
|
60
|
+
"src",
|
|
61
|
+
"!src/**/*.test.ts",
|
|
62
|
+
"!src/**/*.test.tsx",
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE"
|
|
65
|
+
],
|
|
66
|
+
"scripts": {
|
|
67
|
+
"check": "tsc --noEmit",
|
|
68
|
+
"check-types": "tsc --noEmit",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest"
|
|
71
|
+
},
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"@djangocfg/widget-kit": "^0.1.1",
|
|
74
|
+
"@radix-ui/react-direction": "^1.1.2",
|
|
75
|
+
"@radix-ui/react-slot": "^1.3.0",
|
|
76
|
+
"class-variance-authority": "^0.7.1",
|
|
77
|
+
"qrcode": "^1.5.4",
|
|
78
|
+
"react-lottie-player": "^2.1.0"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@djangocfg/i18n": "^2.1.542",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
83
|
+
"lucide-react": "^0.545.0",
|
|
84
|
+
"react": "^19.0.0",
|
|
85
|
+
"react-dom": "^19.0.0"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@djangocfg/i18n": "^2.1.542",
|
|
89
|
+
"@djangocfg/typescript-config": "^2.1.542",
|
|
90
|
+
"@djangocfg/ui-core": "^2.1.542",
|
|
91
|
+
"@storybook/react-vite": "^10.5.0",
|
|
92
|
+
"@types/node": "^25.9.5",
|
|
93
|
+
"@types/qrcode": "^1.5.6",
|
|
94
|
+
"@types/react": "19.2.15",
|
|
95
|
+
"@types/react-dom": "19.2.3",
|
|
96
|
+
"lucide-react": "0.545.0",
|
|
97
|
+
"react": "19.2.7",
|
|
98
|
+
"react-dom": "19.2.7",
|
|
99
|
+
"storybook": "^10.5.0",
|
|
100
|
+
"typescript": "^5.9.3",
|
|
101
|
+
"vitest": "^4.1.10"
|
|
102
|
+
},
|
|
103
|
+
"publishConfig": {
|
|
104
|
+
"access": "public"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
//
|
|
3
|
+
// Grid of color swatches with click-to-copy in hex / rgb / hsl.
|
|
4
|
+
// Pulls colors from one of three sources via `mode`:
|
|
5
|
+
// - 'theme' → useThemePalette() (semantic tokens of the active theme)
|
|
6
|
+
// - 'preset' → useStylePresets() (status preset families)
|
|
7
|
+
// - 'custom' → caller-provided `colors` array
|
|
8
|
+
//
|
|
9
|
+
// Chrome (frame, value label) uses semantic Tailwind tokens; the swatch
|
|
10
|
+
// background uses inline `style={{ backgroundColor: hex }}` so it stays
|
|
11
|
+
// honest regardless of the active theme. See CONTRACT.md §2/§3.
|
|
12
|
+
|
|
13
|
+
'use client';
|
|
14
|
+
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
|
+
import { useThemePalette, useStylePresets } from '@djangocfg/ui-core/styles/palette';
|
|
18
|
+
import { Swatch } from './components/Swatch';
|
|
19
|
+
import type { ColorEntry, ColorPaletteMode, ColorPaletteProps } from './types';
|
|
20
|
+
|
|
21
|
+
function resolveMode(
|
|
22
|
+
mode: ColorPaletteMode | undefined,
|
|
23
|
+
colors: ColorEntry[] | undefined,
|
|
24
|
+
): ColorPaletteMode {
|
|
25
|
+
if (mode) return mode;
|
|
26
|
+
return colors && colors.length > 0 ? 'custom' : 'theme';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the swatch list for `mode === 'theme'` — semantic tokens of the
|
|
31
|
+
* current theme. Includes the foreground/background pair plus brand and
|
|
32
|
+
* status families so a docs page can show "what does this preset look
|
|
33
|
+
* like" at a glance.
|
|
34
|
+
*/
|
|
35
|
+
function useThemeEntries(): ColorEntry[] {
|
|
36
|
+
const palette = useThemePalette();
|
|
37
|
+
return React.useMemo<ColorEntry[]>(
|
|
38
|
+
() => [
|
|
39
|
+
{ name: 'Background', value: palette.background },
|
|
40
|
+
{ name: 'Foreground', value: palette.foreground },
|
|
41
|
+
{ name: 'Card', value: palette.card },
|
|
42
|
+
{ name: 'Muted', value: palette.muted },
|
|
43
|
+
{ name: 'Muted Foreground', value: palette.mutedForeground },
|
|
44
|
+
{ name: 'Border', value: palette.border },
|
|
45
|
+
{ name: 'Primary', value: palette.primary },
|
|
46
|
+
{ name: 'Secondary', value: palette.secondary },
|
|
47
|
+
{ name: 'Accent', value: palette.accent },
|
|
48
|
+
{ name: 'Destructive', value: palette.destructive },
|
|
49
|
+
{ name: 'Success', value: palette.success },
|
|
50
|
+
{ name: 'Warning', value: palette.warning },
|
|
51
|
+
{ name: 'Info', value: palette.info },
|
|
52
|
+
],
|
|
53
|
+
[palette],
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the swatch list for `mode === 'preset'` — the status / brand
|
|
59
|
+
* families exposed by `useStylePresets()`. Renders the `fill` color
|
|
60
|
+
* for each preset; consumers needing stroke/foreground should query
|
|
61
|
+
* the preset hooks directly.
|
|
62
|
+
*/
|
|
63
|
+
function usePresetEntries(): ColorEntry[] {
|
|
64
|
+
const presets = useStylePresets();
|
|
65
|
+
return React.useMemo<ColorEntry[]>(
|
|
66
|
+
() => [
|
|
67
|
+
{ name: 'Primary', value: presets.primary.fill },
|
|
68
|
+
{ name: 'Success', value: presets.success.fill },
|
|
69
|
+
{ name: 'Warning', value: presets.warning.fill },
|
|
70
|
+
{ name: 'Danger', value: presets.danger.fill },
|
|
71
|
+
{ name: 'Info', value: presets.info.fill },
|
|
72
|
+
{ name: 'Chart 3', value: presets.chart3.fill },
|
|
73
|
+
{ name: 'Chart 4', value: presets.chart4.fill },
|
|
74
|
+
{ name: 'Chart 5', value: presets.chart5.fill },
|
|
75
|
+
],
|
|
76
|
+
[presets],
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function ColorPalette({
|
|
81
|
+
mode,
|
|
82
|
+
colors,
|
|
83
|
+
columns = 4,
|
|
84
|
+
copyFormat = 'hex',
|
|
85
|
+
showName = true,
|
|
86
|
+
showValue = true,
|
|
87
|
+
className,
|
|
88
|
+
style,
|
|
89
|
+
...rest
|
|
90
|
+
}: ColorPaletteProps) {
|
|
91
|
+
// Always call both hooks so React keeps a stable order regardless of `mode`.
|
|
92
|
+
const themeEntries = useThemeEntries();
|
|
93
|
+
const presetEntries = usePresetEntries();
|
|
94
|
+
|
|
95
|
+
const resolvedMode = resolveMode(mode, colors);
|
|
96
|
+
const entries = React.useMemo<ColorEntry[]>(() => {
|
|
97
|
+
if (resolvedMode === 'theme') return themeEntries;
|
|
98
|
+
if (resolvedMode === 'preset') return presetEntries;
|
|
99
|
+
return colors ?? [];
|
|
100
|
+
}, [resolvedMode, themeEntries, presetEntries, colors]);
|
|
101
|
+
|
|
102
|
+
const gridStyle = React.useMemo<React.CSSProperties>(
|
|
103
|
+
() => ({
|
|
104
|
+
...style,
|
|
105
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
106
|
+
}),
|
|
107
|
+
[style, columns],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div
|
|
112
|
+
data-slot="color-palette"
|
|
113
|
+
data-mode={resolvedMode}
|
|
114
|
+
className={cn('grid gap-3', className)}
|
|
115
|
+
style={gridStyle}
|
|
116
|
+
{...rest}
|
|
117
|
+
>
|
|
118
|
+
{entries.map((color) => (
|
|
119
|
+
<Swatch
|
|
120
|
+
key={`${color.name}-${color.value}`}
|
|
121
|
+
color={color}
|
|
122
|
+
copyFormat={copyFormat}
|
|
123
|
+
showName={showName}
|
|
124
|
+
showValue={showValue}
|
|
125
|
+
/>
|
|
126
|
+
))}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# ColorPalette
|
|
2
|
+
|
|
3
|
+
Grid of color swatches with copy-on-click. Source can be the active theme, a status preset family, or a custom list.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { ColorPalette } from '@djangocfg/ui-tools/color-palette';
|
|
7
|
+
|
|
8
|
+
// Render the current theme tokens
|
|
9
|
+
<ColorPalette mode="theme" columns={4} />
|
|
10
|
+
|
|
11
|
+
// Custom swatches
|
|
12
|
+
<ColorPalette
|
|
13
|
+
mode="custom"
|
|
14
|
+
colors={[{ name: 'Brand', value: '#0989aa' }]}
|
|
15
|
+
copyFormat="hex"
|
|
16
|
+
/>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
| Prop | Type | Default | Description |
|
|
22
|
+
|---|---|---|---|
|
|
23
|
+
| `mode` | `'theme' \| 'preset' \| 'custom'` | `'custom'` if `colors` provided, else `'theme'` | Where swatches come from. |
|
|
24
|
+
| `colors` | `ColorEntry[]` | — | Required when `mode === 'custom'`. |
|
|
25
|
+
| `columns` | `number` | `4` | Grid columns. |
|
|
26
|
+
| `copyFormat` | `'hex' \| 'rgb' \| 'hsl'` | `'hex'` | Format of the copied + rendered value. |
|
|
27
|
+
| `showName` | `boolean` | `true` | Show the color label. |
|
|
28
|
+
| `showValue` | `boolean` | `true` | Show the color value string. |
|
|
29
|
+
|
|
30
|
+
Storybook: `apps/storybook/stories/ui-tools/visual/ColorPalette.stories.tsx`
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
Adapted from jalcoui (MIT).
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import {
|
|
3
|
+
ColorPalette,
|
|
4
|
+
type ColorEntry as ColorPaletteEntry,
|
|
5
|
+
type ColorFormat as ColorPaletteFormat,
|
|
6
|
+
} from '.';
|
|
7
|
+
|
|
8
|
+
const CUSTOM_COLORS: ColorPaletteEntry[] = [
|
|
9
|
+
{ name: 'Brand Aqua', value: '#0989aa' },
|
|
10
|
+
{ name: 'Brand Lime', value: '#22c55e' },
|
|
11
|
+
{ name: 'Brand Coral', value: '#ef4444' },
|
|
12
|
+
{ name: 'Brand Gold', value: '#f59e0b' },
|
|
13
|
+
{ name: 'Brand Slate', value: '#1e293b' },
|
|
14
|
+
{ name: 'Brand Cloud', value: '#f8fafc' },
|
|
15
|
+
{ name: 'Brand Plum', value: '#8b5cf6' },
|
|
16
|
+
{ name: 'Brand Rose', value: '#ec4899' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const FORMATS: ColorPaletteFormat[] = ['hex', 'rgb', 'hsl'];
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
title: 'Widgets/Visual/ColorPalette',
|
|
23
|
+
component: ColorPalette,
|
|
24
|
+
tags: ['autodocs'],
|
|
25
|
+
parameters: {
|
|
26
|
+
docs: {
|
|
27
|
+
description: {
|
|
28
|
+
component:
|
|
29
|
+
'Grid of color swatches with click-to-copy. Source colors come from the active theme (`mode="theme"`), the status preset family (`mode="preset"`), or a custom array. Copy format toggles between hex / rgb / hsl. Swatch chrome (frame, value label) uses semantic Tailwind tokens; the swatch background is rendered from the raw hex so the displayed color matches the copied value.',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
argTypes: {
|
|
34
|
+
mode: { control: 'select', options: ['theme', 'preset', 'custom'] },
|
|
35
|
+
columns: { control: { type: 'number', min: 1, max: 8, step: 1 } },
|
|
36
|
+
copyFormat: { control: 'select', options: FORMATS },
|
|
37
|
+
showName: { control: 'boolean' },
|
|
38
|
+
showValue: { control: 'boolean' },
|
|
39
|
+
},
|
|
40
|
+
} satisfies Meta<typeof ColorPalette>;
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
type Story = StoryObj<typeof meta>;
|
|
44
|
+
|
|
45
|
+
export const ThemeColors: Story = {
|
|
46
|
+
args: { mode: 'theme', columns: 4, copyFormat: 'hex' },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const StatusPresets: Story = {
|
|
50
|
+
args: { mode: 'preset', columns: 4, copyFormat: 'hex' },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const CustomColors: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
mode: 'custom',
|
|
56
|
+
colors: CUSTOM_COLORS,
|
|
57
|
+
columns: 4,
|
|
58
|
+
copyFormat: 'hex',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const AllFormats: Story = {
|
|
63
|
+
args: {
|
|
64
|
+
mode: 'custom',
|
|
65
|
+
colors: CUSTOM_COLORS,
|
|
66
|
+
columns: 4,
|
|
67
|
+
copyFormat: 'hex',
|
|
68
|
+
},
|
|
69
|
+
render: () => (
|
|
70
|
+
<div className="flex flex-col gap-6">
|
|
71
|
+
{FORMATS.map((fmt) => (
|
|
72
|
+
<div key={fmt} className="flex flex-col gap-2">
|
|
73
|
+
<h3 className="text-sm font-medium uppercase tracking-wide text-muted-foreground">
|
|
74
|
+
copyFormat = "{fmt}"
|
|
75
|
+
</h3>
|
|
76
|
+
<ColorPalette
|
|
77
|
+
mode="custom"
|
|
78
|
+
colors={CUSTOM_COLORS}
|
|
79
|
+
columns={4}
|
|
80
|
+
copyFormat={fmt}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const CompactGrid: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
mode: 'theme',
|
|
91
|
+
columns: 6,
|
|
92
|
+
copyFormat: 'hex',
|
|
93
|
+
showValue: false,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Check, Copy } from 'lucide-react';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
import { useCopyToClipboard } from '../hooks/useCopyToClipboard';
|
|
9
|
+
import { formatColor, getReadableContrast } from '../utils';
|
|
10
|
+
import type { ColorEntry, ColorFormat } from '../types';
|
|
11
|
+
|
|
12
|
+
export interface SwatchProps {
|
|
13
|
+
color: ColorEntry;
|
|
14
|
+
copyFormat: ColorFormat;
|
|
15
|
+
showName: boolean;
|
|
16
|
+
showValue: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Single color swatch — colored block on top, optional `name` +
|
|
21
|
+
* formatted `value` row below. Click anywhere copies the value in
|
|
22
|
+
* the configured {@link ColorFormat}.
|
|
23
|
+
*
|
|
24
|
+
* Background uses inline `style={{ backgroundColor }}` (hex) so the
|
|
25
|
+
* color is rendered verbatim — not via Tailwind's color tokens.
|
|
26
|
+
* Chrome (border, value text) flows through semantic tokens so the
|
|
27
|
+
* frame still respects the active theme preset.
|
|
28
|
+
*/
|
|
29
|
+
export function Swatch({ color, copyFormat, showName, showValue }: SwatchProps) {
|
|
30
|
+
const { copied, copy } = useCopyToClipboard();
|
|
31
|
+
const formattedValue = React.useMemo(
|
|
32
|
+
() => formatColor(color.value, copyFormat),
|
|
33
|
+
[color.value, copyFormat],
|
|
34
|
+
);
|
|
35
|
+
const contrast = React.useMemo(
|
|
36
|
+
() => getReadableContrast(color.value),
|
|
37
|
+
[color.value],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const handleCopy = React.useCallback(() => {
|
|
41
|
+
void copy(formattedValue);
|
|
42
|
+
}, [copy, formattedValue]);
|
|
43
|
+
|
|
44
|
+
const iconColor = contrast === 'light' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(255, 255, 255, 0.85)';
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
type="button"
|
|
49
|
+
onClick={handleCopy}
|
|
50
|
+
data-slot="color-swatch"
|
|
51
|
+
data-copied={copied || undefined}
|
|
52
|
+
aria-label={`Copy ${color.name}: ${formattedValue}`}
|
|
53
|
+
title={color.description ?? `${color.name} — ${formattedValue}`}
|
|
54
|
+
className={cn(
|
|
55
|
+
'group relative flex flex-col overflow-hidden rounded-lg border border-border/60 bg-card text-left shadow-xs outline-none transition-colors',
|
|
56
|
+
'hover:border-border focus-visible:ring-[3px] focus-visible:ring-ring/50',
|
|
57
|
+
)}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
className="relative flex h-20 items-center justify-center sm:h-24"
|
|
61
|
+
style={{ backgroundColor: color.value }}
|
|
62
|
+
>
|
|
63
|
+
{/* Copy / Check icon — hex color resolved in JS, theme tokens skipped on purpose
|
|
64
|
+
because the swatch background is the user-supplied hex, not a token. */}
|
|
65
|
+
<span
|
|
66
|
+
className={cn(
|
|
67
|
+
'inline-flex items-center justify-center rounded-full p-1.5 transition-opacity',
|
|
68
|
+
copied ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100',
|
|
69
|
+
)}
|
|
70
|
+
style={{ color: iconColor }}
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
>
|
|
73
|
+
{copied ? <Check className="size-4" /> : <Copy className="size-4" />}
|
|
74
|
+
</span>
|
|
75
|
+
|
|
76
|
+
{copied && (
|
|
77
|
+
<span
|
|
78
|
+
data-slot="color-swatch-toast"
|
|
79
|
+
className="absolute bottom-1.5 right-1.5 rounded-md bg-card/90 px-1.5 py-0.5 text-[10px] font-medium text-foreground shadow-sm backdrop-blur"
|
|
80
|
+
>
|
|
81
|
+
Copied!
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{(showName || showValue) && (
|
|
87
|
+
<div className="flex flex-col gap-0.5 bg-card px-3 py-2">
|
|
88
|
+
{showName && (
|
|
89
|
+
<span className="truncate text-xs font-medium text-foreground">
|
|
90
|
+
{color.name}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{showValue && (
|
|
94
|
+
<span className="truncate font-mono text-[11px] text-muted-foreground">
|
|
95
|
+
{formattedValue}
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</button>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Copy a string to the clipboard and flash a `copied` flag for
|
|
9
|
+
* `resetMs` (default 1500ms). Safe to call on browsers without
|
|
10
|
+
* `navigator.clipboard` — returns silently.
|
|
11
|
+
*/
|
|
12
|
+
export function useCopyToClipboard(resetMs = 1500): {
|
|
13
|
+
copied: boolean;
|
|
14
|
+
copy: (text: string) => Promise<void>;
|
|
15
|
+
} {
|
|
16
|
+
const [copied, setCopied] = React.useState(false);
|
|
17
|
+
const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
18
|
+
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
return () => {
|
|
21
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
22
|
+
};
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const copy = React.useCallback(
|
|
26
|
+
async (text: string) => {
|
|
27
|
+
try {
|
|
28
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard) return;
|
|
29
|
+
await navigator.clipboard.writeText(text);
|
|
30
|
+
setCopied(true);
|
|
31
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
32
|
+
timerRef.current = setTimeout(() => setCopied(false), resetMs);
|
|
33
|
+
} catch {
|
|
34
|
+
// Clipboard API not available / permission denied — ignore.
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[resetMs],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { copied, copy };
|
|
41
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
export { ColorPalette } from './ColorPalette';
|
|
4
|
+
export { Swatch as ColorPaletteSwatch } from './components/Swatch';
|
|
5
|
+
export { useCopyToClipboard } from './hooks/useCopyToClipboard';
|
|
6
|
+
export { formatColor, getReadableContrast } from './utils';
|
|
7
|
+
export type {
|
|
8
|
+
ColorEntry,
|
|
9
|
+
ColorFormat,
|
|
10
|
+
ColorPaletteMode,
|
|
11
|
+
ColorPaletteProps,
|
|
12
|
+
} from './types';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
import { createLazyComponent } from '@djangocfg/widget-kit/lazy';
|
|
6
|
+
import type { ColorPaletteProps } from './types';
|
|
7
|
+
|
|
8
|
+
export const LazyColorPalette = createLazyComponent<ColorPaletteProps>(
|
|
9
|
+
() => import('./ColorPalette').then((m) => ({ default: m.ColorPalette })),
|
|
10
|
+
{
|
|
11
|
+
displayName: 'LazyColorPalette',
|
|
12
|
+
fallback: (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="color-palette-skeleton"
|
|
15
|
+
className="grid h-32 animate-pulse gap-3 rounded-lg bg-muted"
|
|
16
|
+
/>
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export type { ColorPaletteProps } from './types';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Adapted from jalcoui (MIT) — github.com/jal-co/ui
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Display format for the copied / rendered color value.
|
|
7
|
+
*
|
|
8
|
+
* - `hex` — `#0989aa` (default). Canvas2D / SVG friendly.
|
|
9
|
+
* - `rgb` — `rgb(9, 137, 170)`.
|
|
10
|
+
* - `hsl` — `hsl(192, 90%, 35%)`.
|
|
11
|
+
*/
|
|
12
|
+
export type ColorFormat = 'hex' | 'rgb' | 'hsl';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single color entry rendered as one swatch.
|
|
16
|
+
*
|
|
17
|
+
* `value` is always a hex string (`#rrggbb`). Resolve theme tokens
|
|
18
|
+
* to hex via `useThemeColor(...)` before passing them in.
|
|
19
|
+
*/
|
|
20
|
+
export interface ColorEntry {
|
|
21
|
+
/** Display name (e.g. "Primary", "Success", "brand-accent"). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Hex color value, e.g. `#0989aa`. */
|
|
24
|
+
value: string;
|
|
25
|
+
/** Optional usage note. */
|
|
26
|
+
description?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Source of swatches.
|
|
31
|
+
*
|
|
32
|
+
* - `theme` — semantic tokens from `useThemePalette()` (background,
|
|
33
|
+
* foreground, primary, secondary, muted, accent, destructive, …).
|
|
34
|
+
* - `preset` — status preset family from `useStylePresets()`
|
|
35
|
+
* (primary, success, warning, danger, info, chart3-5).
|
|
36
|
+
* - `custom` — caller-provided `colors` array.
|
|
37
|
+
*/
|
|
38
|
+
export type ColorPaletteMode = 'theme' | 'preset' | 'custom';
|
|
39
|
+
|
|
40
|
+
export interface ColorPaletteProps
|
|
41
|
+
extends Omit<React.ComponentProps<'div'>, 'children'> {
|
|
42
|
+
/**
|
|
43
|
+
* Where swatches come from. Defaults to `'custom'` when `colors` is
|
|
44
|
+
* provided, otherwise `'theme'`.
|
|
45
|
+
*/
|
|
46
|
+
mode?: ColorPaletteMode;
|
|
47
|
+
/**
|
|
48
|
+
* Explicit list of swatches (required when `mode === 'custom'`).
|
|
49
|
+
* Ignored for `'theme'` / `'preset'`.
|
|
50
|
+
*/
|
|
51
|
+
colors?: ColorEntry[];
|
|
52
|
+
/** Grid column count. @default 4 */
|
|
53
|
+
columns?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Format of the copied value AND the rendered value text.
|
|
56
|
+
* @default 'hex'
|
|
57
|
+
*/
|
|
58
|
+
copyFormat?: ColorFormat;
|
|
59
|
+
/** Show the color name label below the swatch. @default true */
|
|
60
|
+
showName?: boolean;
|
|
61
|
+
/** Show the color value string below the swatch. @default true */
|
|
62
|
+
showValue?: boolean;
|
|
63
|
+
}
|