@redseed/redseed-ui-vue3 8.32.2 → 8.33.0

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/index.js CHANGED
@@ -12,6 +12,7 @@ export * from './src/components/Button'
12
12
  export * from './src/components/ButtonGroup'
13
13
  export * from './src/components/Card'
14
14
  export * from './src/components/CardGroup'
15
+ export * from './src/components/Chart'
15
16
  export * from './src/components/Comment'
16
17
  export * from './src/components/Disclosure'
17
18
  export * from './src/components/Drawer'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseed/redseed-ui-vue3",
3
- "version": "8.32.2",
3
+ "version": "8.33.0",
4
4
  "description": "RedSeed UI Vue 3 components",
5
5
  "main": "index.js",
6
6
  "repository": "https://github.com/redseedtraining/redseed-ui",
@@ -12,6 +12,8 @@
12
12
  "license": "ISC",
13
13
  "dependencies": {
14
14
  "@heroicons/vue": "2.2.0",
15
+ "apexcharts": "5.13.0",
16
+ "vue3-apexcharts": "1.11.1",
15
17
  "@vueuse/components": "14.2.1",
16
18
  "@vueuse/core": "14.2.1",
17
19
  "@vueuse/integrations": "14.2.1",
@@ -0,0 +1,68 @@
1
+ <script setup>
2
+ // ApexCharts touches window/DOM, so this component renders client-side only
3
+ // (guarded by the `mounted` ref). VueApexCharts is imported and used locally
4
+ // so consumers don't need to install the global plugin via app.use().
5
+ import { ref, computed, onMounted } from 'vue'
6
+ import VueApexCharts from 'vue3-apexcharts'
7
+ import { resolveTheme, baseChartOptions, deepMergeOptions } from './chartTheme'
8
+
9
+ const props = defineProps({
10
+ // ApexCharts bar series shape: [{ name, data: [...] }]
11
+ series: {
12
+ type: Array,
13
+ default: () => [],
14
+ },
15
+ // x-axis category labels
16
+ categories: {
17
+ type: Array,
18
+ default: () => [],
19
+ },
20
+ height: {
21
+ type: [Number, String],
22
+ default: 350,
23
+ },
24
+ // Stack the series instead of grouping them side by side
25
+ stacked: {
26
+ type: Boolean,
27
+ default: false,
28
+ },
29
+ // Escape hatch — deep-merged over the rsui defaults
30
+ options: {
31
+ type: Object,
32
+ default: () => ({}),
33
+ },
34
+ })
35
+
36
+ const root = ref(null)
37
+ const mounted = ref(false)
38
+ const theme = ref(resolveTheme(null))
39
+
40
+ onMounted(() => {
41
+ theme.value = resolveTheme(root.value)
42
+ mounted.value = true
43
+ })
44
+
45
+ const chartOptions = computed(() =>
46
+ deepMergeOptions(
47
+ baseChartOptions({
48
+ type: 'bar',
49
+ theme: theme.value,
50
+ height: props.height,
51
+ categories: props.categories,
52
+ stacked: props.stacked,
53
+ }),
54
+ props.options,
55
+ ),
56
+ )
57
+ </script>
58
+ <template>
59
+ <div ref="root" class="rsui-chart">
60
+ <VueApexCharts
61
+ v-if="mounted"
62
+ type="bar"
63
+ :height="height"
64
+ :series="series"
65
+ :options="chartOptions"
66
+ />
67
+ </div>
68
+ </template>
@@ -0,0 +1,62 @@
1
+ <script setup>
2
+ // ApexCharts touches window/DOM, so this component renders client-side only
3
+ // (guarded by the `mounted` ref). VueApexCharts is imported and used locally
4
+ // so consumers don't need to install the global plugin via app.use().
5
+ import { ref, computed, onMounted } from 'vue'
6
+ import VueApexCharts from 'vue3-apexcharts'
7
+ import { resolveTheme, baseChartOptions, deepMergeOptions } from './chartTheme'
8
+
9
+ const props = defineProps({
10
+ // Donut series is a flat array of numbers, one per slice
11
+ series: {
12
+ type: Array,
13
+ default: () => [],
14
+ },
15
+ // Slice labels, parallel to `series`
16
+ labels: {
17
+ type: Array,
18
+ default: () => [],
19
+ },
20
+ height: {
21
+ type: [Number, String],
22
+ default: 350,
23
+ },
24
+ // Escape hatch — deep-merged over the rsui defaults
25
+ options: {
26
+ type: Object,
27
+ default: () => ({}),
28
+ },
29
+ })
30
+
31
+ const root = ref(null)
32
+ const mounted = ref(false)
33
+ const theme = ref(resolveTheme(null))
34
+
35
+ onMounted(() => {
36
+ theme.value = resolveTheme(root.value)
37
+ mounted.value = true
38
+ })
39
+
40
+ const chartOptions = computed(() =>
41
+ deepMergeOptions(
42
+ baseChartOptions({
43
+ type: 'donut',
44
+ theme: theme.value,
45
+ height: props.height,
46
+ labels: props.labels,
47
+ }),
48
+ props.options,
49
+ ),
50
+ )
51
+ </script>
52
+ <template>
53
+ <div ref="root" class="rsui-chart">
54
+ <VueApexCharts
55
+ v-if="mounted"
56
+ type="donut"
57
+ :height="height"
58
+ :series="series"
59
+ :options="chartOptions"
60
+ />
61
+ </div>
62
+ </template>
@@ -0,0 +1,62 @@
1
+ <script setup>
2
+ // ApexCharts touches window/DOM, so this component renders client-side only
3
+ // (guarded by the `mounted` ref). VueApexCharts is imported and used locally
4
+ // so consumers don't need to install the global plugin via app.use().
5
+ import { ref, computed, onMounted } from 'vue'
6
+ import VueApexCharts from 'vue3-apexcharts'
7
+ import { resolveTheme, baseChartOptions, deepMergeOptions } from './chartTheme'
8
+
9
+ const props = defineProps({
10
+ // ApexCharts line series shape: [{ name, data: [...] }]
11
+ series: {
12
+ type: Array,
13
+ default: () => [],
14
+ },
15
+ // x-axis category labels
16
+ categories: {
17
+ type: Array,
18
+ default: () => [],
19
+ },
20
+ height: {
21
+ type: [Number, String],
22
+ default: 350,
23
+ },
24
+ // Escape hatch — deep-merged over the rsui defaults
25
+ options: {
26
+ type: Object,
27
+ default: () => ({}),
28
+ },
29
+ })
30
+
31
+ const root = ref(null)
32
+ const mounted = ref(false)
33
+ const theme = ref(resolveTheme(null))
34
+
35
+ onMounted(() => {
36
+ theme.value = resolveTheme(root.value)
37
+ mounted.value = true
38
+ })
39
+
40
+ const chartOptions = computed(() =>
41
+ deepMergeOptions(
42
+ baseChartOptions({
43
+ type: 'line',
44
+ theme: theme.value,
45
+ height: props.height,
46
+ categories: props.categories,
47
+ }),
48
+ props.options,
49
+ ),
50
+ )
51
+ </script>
52
+ <template>
53
+ <div ref="root" class="rsui-chart">
54
+ <VueApexCharts
55
+ v-if="mounted"
56
+ type="line"
57
+ :height="height"
58
+ :series="series"
59
+ :options="chartOptions"
60
+ />
61
+ </div>
62
+ </template>
@@ -0,0 +1,161 @@
1
+ import { mergeWith } from 'lodash'
2
+
3
+ /**
4
+ * Shared theming for the rsui chart components (LineChart / BarChart /
5
+ * DoughnutChart). These wrap ApexCharts, which renders its own SVG and is
6
+ * styled through JS options rather than CSS classes — so the rsui look lives
7
+ * here as an options object built from the library's design tokens.
8
+ *
9
+ * Tokens are CSS custom properties (HSLA) defined in the tailwindcss package.
10
+ * We read them at runtime via getComputedStyle so charts automatically follow
11
+ * dark mode and any per-app brand overrides. The static fallbacks below mirror
12
+ * the token values in `packages/tailwindcss/colors.css` and cover environments
13
+ * where getComputedStyle returns nothing (SSR, happy-dom tests, pre-mount).
14
+ */
15
+
16
+ // Default categorical/series palette, in order. First six are the semantic
17
+ // brand/info/success/warning/error/ai colours; the remaining shades extend the
18
+ // palette when a chart has more than six series.
19
+ const PALETTE_TOKENS = [
20
+ { name: '--Colors-Brand-500', fallback: 'hsla(271, 48%, 24%, 1)' },
21
+ { name: '--Colors-RedSeed-Blue-500', fallback: 'hsla(205, 100%, 40%, 1)' },
22
+ { name: '--Colors-RedSeed-Green-500', fallback: 'hsla(162, 43%, 44%, 1)' },
23
+ { name: '--Colors-RedSeed-Yellow-500', fallback: 'hsla(38, 100%, 51%, 1)' },
24
+ { name: '--Colors-RedSeed-Red-500', fallback: 'hsla(0, 85%, 44%, 1)' },
25
+ { name: '--Colors-RedSeed-Purple-500', fallback: 'hsla(271, 70%, 60%, 1)' },
26
+ { name: '--Colors-Brand-400', fallback: 'hsla(271, 23%, 39%, 1)' },
27
+ { name: '--Colors-RedSeed-Blue-600', fallback: 'hsla(205, 100%, 36%, 1)' },
28
+ { name: '--Colors-RedSeed-Green-600', fallback: 'hsla(162, 43%, 40%, 1)' },
29
+ { name: '--Colors-RedSeed-Yellow-600', fallback: 'hsla(38, 96%, 46%, 1)' },
30
+ { name: '--Colors-RedSeed-Red-600', fallback: 'hsla(0, 85%, 40%, 1)' },
31
+ { name: '--Colors-RedSeed-Purple-600', fallback: 'hsla(271, 55%, 54%, 1)' },
32
+ ]
33
+
34
+ const TEXT_TOKEN = { name: '--Colors-Grey-900', fallback: 'hsla(220, 24%, 12%, 1)' }
35
+ const TEXT_MUTED_TOKEN = { name: '--Colors-Grey-700', fallback: 'hsla(220, 11%, 29%, 1)' }
36
+ const GRID_TOKEN = { name: '--Colors-Grey-200', fallback: 'hsla(220, 5%, 92%, 1)' }
37
+ const FONT_TOKEN = { name: '--font-redseed', fallback: "'Inter', sans-serif" }
38
+
39
+ function readToken(el, token) {
40
+ if (typeof window === 'undefined' || !el) return token.fallback
41
+ const value = getComputedStyle(el).getPropertyValue(token.name).trim()
42
+ return value || token.fallback
43
+ }
44
+
45
+ /**
46
+ * Resolve the rsui design tokens into concrete colour/font values.
47
+ * Pass the chart's root element so per-subtree CSS-var overrides apply;
48
+ * falls back to the static palette when no element is available.
49
+ */
50
+ export function resolveTheme(el) {
51
+ return {
52
+ colors: PALETTE_TOKENS.map((token) => readToken(el, token)),
53
+ text: readToken(el, TEXT_TOKEN),
54
+ textMuted: readToken(el, TEXT_MUTED_TOKEN),
55
+ grid: readToken(el, GRID_TOKEN),
56
+ fontFamily: readToken(el, FONT_TOKEN),
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Build the rsui ApexCharts defaults for a given chart type. Consumers layer
62
+ * their own `options` over the result via deepMergeOptions().
63
+ *
64
+ * @param {'line'|'bar'|'donut'} type
65
+ * @param {object} theme result of resolveTheme()
66
+ * @param {number|string} height
67
+ * @param {string[]} [categories] x-axis labels (line/bar)
68
+ * @param {string[]} [labels] slice labels (donut)
69
+ * @param {boolean} [stacked] bar only
70
+ */
71
+ export function baseChartOptions({ type, theme, height, categories, labels, stacked = false }) {
72
+ const options = {
73
+ chart: {
74
+ type,
75
+ height,
76
+ fontFamily: theme.fontFamily,
77
+ foreColor: theme.textMuted,
78
+ toolbar: { show: false },
79
+ zoom: { enabled: false },
80
+ },
81
+ colors: theme.colors,
82
+ dataLabels: { enabled: false },
83
+ legend: {
84
+ position: 'bottom',
85
+ horizontalAlign: 'center',
86
+ fontFamily: theme.fontFamily,
87
+ labels: { colors: theme.text },
88
+ markers: { strokeWidth: 0 },
89
+ itemMargin: { horizontal: 8, vertical: 4 },
90
+ },
91
+ tooltip: {
92
+ followCursor: true,
93
+ style: { fontFamily: theme.fontFamily },
94
+ },
95
+ }
96
+
97
+ if (type === 'line') {
98
+ options.chart.stacked = false
99
+ options.stroke = { curve: 'smooth', width: 3 }
100
+ options.markers = { size: 0, hover: { size: 5 } }
101
+ options.grid = { borderColor: theme.grid, strokeDashArray: 4 }
102
+ options.xaxis = axis(categories, theme)
103
+ options.yaxis = { labels: { style: { colors: theme.textMuted } } }
104
+ }
105
+
106
+ if (type === 'bar') {
107
+ options.chart.stacked = !!stacked
108
+ options.stroke = { show: stacked, width: stacked ? 1 : 0, colors: ['transparent'] }
109
+ options.plotOptions = {
110
+ bar: {
111
+ borderRadius: 6,
112
+ borderRadiusApplication: stacked ? 'around' : 'end',
113
+ columnWidth: '55%',
114
+ },
115
+ }
116
+ options.grid = { borderColor: theme.grid, strokeDashArray: 4 }
117
+ options.xaxis = axis(categories, theme)
118
+ options.yaxis = { labels: { style: { colors: theme.textMuted } } }
119
+ }
120
+
121
+ if (type === 'donut') {
122
+ options.labels = labels || []
123
+ options.stroke = { width: 0 }
124
+ options.plotOptions = {
125
+ pie: {
126
+ donut: {
127
+ size: '68%',
128
+ labels: {
129
+ show: true,
130
+ total: {
131
+ show: true,
132
+ color: theme.textMuted,
133
+ fontFamily: theme.fontFamily,
134
+ },
135
+ value: { color: theme.text, fontFamily: theme.fontFamily },
136
+ },
137
+ },
138
+ },
139
+ }
140
+ }
141
+
142
+ return options
143
+ }
144
+
145
+ function axis(categories, theme) {
146
+ return {
147
+ categories: categories || [],
148
+ labels: { style: { colors: theme.textMuted } },
149
+ axisBorder: { color: theme.grid },
150
+ axisTicks: { color: theme.grid },
151
+ }
152
+ }
153
+
154
+ // Arrays from the consumer's options replace defaults wholesale; plain objects
155
+ // continue to deep-merge so partial overrides (e.g. grid.borderColor) still work.
156
+ const replaceArrays = (objValue, srcValue) => (Array.isArray(srcValue) ? srcValue : undefined)
157
+
158
+ /** Deep-merge a consumer `options` object over the rsui defaults. */
159
+ export function deepMergeOptions(base, overrides) {
160
+ return mergeWith({}, base, overrides || {}, replaceArrays)
161
+ }
@@ -0,0 +1,9 @@
1
+ import LineChart from './LineChart.vue'
2
+ import BarChart from './BarChart.vue'
3
+ import DoughnutChart from './DoughnutChart.vue'
4
+
5
+ export {
6
+ LineChart,
7
+ BarChart,
8
+ DoughnutChart,
9
+ }