vccs 1.0.0-beta.2 → 1.0.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/README.md +11 -0
- package/dist/es/cartesian/brush/Brush.mjs +4 -0
- package/package.json +6 -2
- package/usage/catalog.json +235 -0
- package/usage/guides/charts/area-chart.md +56 -0
- package/usage/guides/charts/bar-chart.md +60 -0
- package/usage/guides/charts/bar-list.md +46 -0
- package/usage/guides/charts/calendar-heatmap.md +55 -0
- package/usage/guides/charts/cohort-chart.md +47 -0
- package/usage/guides/charts/composed-chart.md +70 -0
- package/usage/guides/charts/funnel-chart.md +58 -0
- package/usage/guides/charts/heatmap.md +51 -0
- package/usage/guides/charts/journey-sankey.md +49 -0
- package/usage/guides/charts/line-chart.md +60 -0
- package/usage/guides/charts/pie-chart.md +59 -0
- package/usage/guides/charts/radar-chart.md +69 -0
- package/usage/guides/charts/radial-bar-chart.md +54 -0
- package/usage/guides/charts/sankey.md +59 -0
- package/usage/guides/charts/scatter-chart.md +62 -0
- package/usage/guides/charts/sparkline.md +51 -0
- package/usage/guides/charts/sunburst-chart.md +66 -0
- package/usage/guides/charts/tracker.md +49 -0
- package/usage/guides/charts/treemap.md +68 -0
- package/usage/guides/core/installation.md +68 -0
- package/usage/guides/core/ssr-testing.md +50 -0
- package/usage/guides/core/typed-rows.md +47 -0
- package/usage/guides/core/vue-api.md +72 -0
- package/usage/vccs-usage.mjs +191 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Tracker
|
|
2
|
+
|
|
3
|
+
Use `Tracker` for an ordered sequence of discrete statuses, such as daily service health or build results. Use `Sparkline` for a numeric trend and `Heatmap` for a two-dimensional value matrix.
|
|
4
|
+
|
|
5
|
+
## Data shape
|
|
6
|
+
|
|
7
|
+
- `data` contains one row per bar, ordered oldest first.
|
|
8
|
+
- `data-key` selects the status and defaults to `status`.
|
|
9
|
+
- `name-key` selects the label and bar identity; it defaults to `date`.
|
|
10
|
+
- Built-in statuses are `up`, `degraded`, `down`, and `maintenance`; custom status strings need their own colors and labels.
|
|
11
|
+
|
|
12
|
+
## Minimal example
|
|
13
|
+
|
|
14
|
+
```vue
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { Tooltip, Tracker } from 'vccs'
|
|
17
|
+
|
|
18
|
+
const data = [
|
|
19
|
+
{ date: '2026-08-01', status: 'up' },
|
|
20
|
+
{ date: '2026-08-02', status: 'up' },
|
|
21
|
+
{ date: '2026-08-03', status: 'degraded' },
|
|
22
|
+
{ date: '2026-08-04', status: 'up' },
|
|
23
|
+
{ date: '2026-08-05', status: 'down' },
|
|
24
|
+
{ date: '2026-08-06', status: 'up' },
|
|
25
|
+
{ date: '2026-08-07', status: 'maintenance' },
|
|
26
|
+
]
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<Tracker
|
|
31
|
+
:data="data"
|
|
32
|
+
:height="32"
|
|
33
|
+
>
|
|
34
|
+
<Tooltip :cursor="false" />
|
|
35
|
+
</Tracker>
|
|
36
|
+
</template>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Chart-specific rules
|
|
40
|
+
|
|
41
|
+
- Keep input rows oldest first; the component does not sort them into time order.
|
|
42
|
+
- Use a stable `name-key` so bars keep their identity when a moving window drops one row and appends another.
|
|
43
|
+
- For custom statuses, set both `status-colors` and `status-labels`; unknown statuses otherwise use the muted color and their raw text.
|
|
44
|
+
- Place `Tooltip` in the default slot. Cell events receive the original input row.
|
|
45
|
+
- Use the `#cell` slot to replace a bar; its slot cell wraps the domain row rather than being the row itself.
|
|
46
|
+
|
|
47
|
+
## Related guides
|
|
48
|
+
|
|
49
|
+
`installation`, `vue-api`, `typed-rows`, `ssr-testing`, `sparkline`, `heatmap`, `calendar-heatmap`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Treemap
|
|
2
|
+
|
|
3
|
+
Use `Treemap` to compare hierarchical part-to-whole values as rectangles, especially when there are many leaves. Use `PieChart` for a short flat list of proportions and `SunburstChart` when radial rings communicate the hierarchy better.
|
|
4
|
+
|
|
5
|
+
## Data shape
|
|
6
|
+
|
|
7
|
+
- `data` is an array of top-level nodes, not a single root object.
|
|
8
|
+
- Each node has a `name` by default and may have a `children` array.
|
|
9
|
+
- Leaves need a positive numeric `value`; `data-key` and `name-key` select different fields.
|
|
10
|
+
- Parent values are derived by summing their descendants, so do not add totals to parent rows.
|
|
11
|
+
|
|
12
|
+
## Minimal example
|
|
13
|
+
|
|
14
|
+
```vue
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
import { Tooltip, Treemap } from 'vccs'
|
|
17
|
+
|
|
18
|
+
const data = [
|
|
19
|
+
{
|
|
20
|
+
name: 'Search',
|
|
21
|
+
children: [
|
|
22
|
+
{ name: 'Google', value: 420 },
|
|
23
|
+
{ name: 'Bing', value: 90 },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'Social',
|
|
28
|
+
children: [
|
|
29
|
+
{ name: 'LinkedIn', value: 150 },
|
|
30
|
+
{ name: 'Mastodon', value: 80 },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Direct',
|
|
35
|
+
children: [
|
|
36
|
+
{ name: 'Bookmarks', value: 160 },
|
|
37
|
+
{ name: 'Newsletter', value: 110 },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const colors = ['#f97316', '#14b8a6', '#f59e0b', '#06b6d4']
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<template>
|
|
46
|
+
<Treemap
|
|
47
|
+
:data="data"
|
|
48
|
+
:height="320"
|
|
49
|
+
data-key="value"
|
|
50
|
+
:colors="colors"
|
|
51
|
+
>
|
|
52
|
+
<Tooltip :cursor="false" />
|
|
53
|
+
</Treemap>
|
|
54
|
+
</template>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Animation is enabled by default; retrieve `ssr-testing` before taking screenshots or asserting exact geometry.
|
|
58
|
+
|
|
59
|
+
## Chart-specific rules
|
|
60
|
+
|
|
61
|
+
- The default `type="flat"` displays leaves. Their `colors` entry comes from their top-level group.
|
|
62
|
+
- Set `type="nest"` for drill-down. Clicking a group shows its children and a breadcrumb trail for going back.
|
|
63
|
+
- Use `stroke` to change the rectangle border color.
|
|
64
|
+
- Customize rectangles and labels with the `#content` slot, not a function-valued `content` prop.
|
|
65
|
+
|
|
66
|
+
## Related guides
|
|
67
|
+
|
|
68
|
+
`installation`, `vue-api`, `ssr-testing`, `sunburst-chart`, `pie-chart`.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Installation and registration
|
|
2
|
+
|
|
3
|
+
vccs requires Vue `^3.5`, the `motion-v` `^2.4` peer, and an ESM bundler. The Nuxt module requires Nuxt 4; the Vite resolver requires `unplugin-vue-components` in the application's dev dependencies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add vccs motion-v
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Install `motion-v` explicitly because not every package manager installs peers automatically.
|
|
12
|
+
|
|
13
|
+
## Explicit imports in Vite
|
|
14
|
+
|
|
15
|
+
Import components from `vccs` and compose them in the template. Retrieve the chart guide before choosing its children.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Bar, BarChart, Tooltip, XAxis, YAxis } from 'vccs'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Do not deep-import source files, runtime prop definitions, or internal helpers. The only public subpaths are `vccs/nuxt` and `vccs/resolver`.
|
|
22
|
+
|
|
23
|
+
## Auto-imports in Vite
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pnpm add -D unplugin-vue-components
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import vue from '@vitejs/plugin-vue'
|
|
31
|
+
import { defineConfig } from 'vite'
|
|
32
|
+
import Components from 'unplugin-vue-components/vite'
|
|
33
|
+
import { VccsResolver } from 'vccs/resolver'
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
plugins: [
|
|
37
|
+
vue(),
|
|
38
|
+
Components({ resolvers: [VccsResolver()] }),
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The resolver imports a component only when the template uses it. Keep explicit imports for types and `defineChartComponents`.
|
|
44
|
+
|
|
45
|
+
## Nuxt 4
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
export default defineNuxtConfig({
|
|
49
|
+
modules: ['vccs/nuxt'],
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The module registers every public component and composable. Import types and `defineChartComponents` from `vccs`.
|
|
54
|
+
|
|
55
|
+
When another UI library also registers `Tooltip`, `Legend`, or `Label`, add a prefix:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export default defineNuxtConfig({
|
|
59
|
+
modules: ['vccs/nuxt'],
|
|
60
|
+
vccs: { prefix: 'V' },
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This registers names such as `VBarChart` and `VLine`. Nuxt 3 is unsupported by the module; do not hide its build error with `transpile` or `ClientOnly` workarounds.
|
|
65
|
+
|
|
66
|
+
## Related guides
|
|
67
|
+
|
|
68
|
+
`vue-api`, `typed-rows`, `ssr-testing`.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SSR and deterministic testing
|
|
2
|
+
|
|
3
|
+
Server-rendered charts normally preserve their entrance start, then animate after hydration when they enter the viewport. Do not mistake that intended entrance state for missing SVG or a hydration failure.
|
|
4
|
+
|
|
5
|
+
## Complete server HTML
|
|
6
|
+
|
|
7
|
+
For a complete chart without client JavaScript, provide numeric `width` and `height` and disable animation on the chart root:
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<AreaChart
|
|
11
|
+
:data="data"
|
|
12
|
+
:width="900"
|
|
13
|
+
:height="250"
|
|
14
|
+
:is-animation-active="false"
|
|
15
|
+
>
|
|
16
|
+
<Area data-key="desktop" />
|
|
17
|
+
</AreaChart>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Disabling animation alone does not reveal an unmeasured responsive chart. With only `height`, the server reserves a box using an initial width and the browser measures the real width later.
|
|
21
|
+
|
|
22
|
+
Do not add `ClientOnly` to solve chart SSR. Use it only when the data itself exists exclusively in the browser.
|
|
23
|
+
|
|
24
|
+
## Nuxt
|
|
25
|
+
|
|
26
|
+
Retrieve `installation` and register `vccs/nuxt` under Nuxt 4. The module handles component and composable registration; fixed-size SSR needs no client-only wrapper.
|
|
27
|
+
|
|
28
|
+
## Deterministic tests and screenshots
|
|
29
|
+
|
|
30
|
+
Set `is-animation-active` to `false` on the chart root, or on an individual series when only that series must be still:
|
|
31
|
+
|
|
32
|
+
```vue
|
|
33
|
+
<BarChart
|
|
34
|
+
:data="rows"
|
|
35
|
+
:height="300"
|
|
36
|
+
:is-animation-active="false"
|
|
37
|
+
>
|
|
38
|
+
<Bar data-key="desktop" />
|
|
39
|
+
</BarChart>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The chart value supplies defaults to its items; an explicit series value overrides it. Disable animation before exact geometry assertions, screenshots, or pixel-exact SSR comparisons.
|
|
43
|
+
|
|
44
|
+
In component tests, measure the container deliberately and assert library behavior through public classes such as `.v-charts-surface` and item attributes such as `data-v-charts-item-index`. Do not depend on generated SVG ids.
|
|
45
|
+
|
|
46
|
+
Charts follow the operating system's reduced-motion setting, but tests must not depend on the host setting: disable animation explicitly.
|
|
47
|
+
|
|
48
|
+
## Related guides
|
|
49
|
+
|
|
50
|
+
`installation`, `vue-api`, `typed-rows`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Typed chart rows
|
|
2
|
+
|
|
3
|
+
A child component cannot infer the row type from its chart root's `data`. Create row-typed component aliases once when wrong `data-key` values or untyped tooltip rows should become compile-time errors.
|
|
4
|
+
|
|
5
|
+
## Minimal example
|
|
6
|
+
|
|
7
|
+
```vue
|
|
8
|
+
<script setup lang="ts">
|
|
9
|
+
import { Area, AreaChart, Tooltip, XAxis, defineChartComponents } from 'vccs'
|
|
10
|
+
|
|
11
|
+
interface Visit {
|
|
12
|
+
date: string
|
|
13
|
+
desktop: number
|
|
14
|
+
mobile: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
defineProps<{ visits: Visit[] }>()
|
|
18
|
+
|
|
19
|
+
const Chart = defineChartComponents<Visit>()({ AreaChart, Area, XAxis, Tooltip })
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<Chart.AreaChart
|
|
24
|
+
:data="visits"
|
|
25
|
+
:height="300"
|
|
26
|
+
>
|
|
27
|
+
<Chart.XAxis data-key="date" />
|
|
28
|
+
<Chart.Area data-key="desktop" />
|
|
29
|
+
<Chart.Area data-key="mobile" />
|
|
30
|
+
<Chart.Tooltip />
|
|
31
|
+
</Chart.AreaChart>
|
|
32
|
+
</template>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`Chart.Area data-key="tablet"` now fails typechecking because `tablet` is not a key of `Visit`. Tooltip content created through `Chart.Tooltip` receives payload entries whose `payload` is `Visit`.
|
|
36
|
+
|
|
37
|
+
## Rules
|
|
38
|
+
|
|
39
|
+
- Select every component used with that row type in one `defineChartComponents<Row>()({ ... })` call; use the resulting aliases in the template.
|
|
40
|
+
- The runtime component references are unchanged. This helper narrows declarations, not behavior.
|
|
41
|
+
- Only selected components are referenced by the consumer bundle.
|
|
42
|
+
- Standalone charts can infer rows from `data`; use their retrieved guide's generic pattern when explicit aliases are unnecessary.
|
|
43
|
+
- Import reusable model and payload types such as `BrushStartEndIndex`, `TooltipActiveIndex`, `TooltipPayloadEntry`, and `TooltipPayload` from `vccs`. Runtime `*VueProps` objects and internal context helpers are not public.
|
|
44
|
+
|
|
45
|
+
## Related guides
|
|
46
|
+
|
|
47
|
+
`installation`, `vue-api`, `ssr-testing`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Vue API rules
|
|
2
|
+
|
|
3
|
+
vccs keeps Recharts component names but uses Vue rendering and reactivity contracts. Apply these rules only after retrieving the chart guide; supported slots and models are component-specific.
|
|
4
|
+
|
|
5
|
+
## Slots replace render props
|
|
6
|
+
|
|
7
|
+
Use named slots for custom shapes, dots, labels, ticks, tooltip content, and cursors. A function passed to a prop such as `dot` or `content` is not a Vue render contract.
|
|
8
|
+
|
|
9
|
+
```vue
|
|
10
|
+
<Tooltip>
|
|
11
|
+
<template #content="{ active, payload, label }">
|
|
12
|
+
<div v-if="active">
|
|
13
|
+
<strong>{{ label }}</strong>
|
|
14
|
+
<div>{{ payload?.[0]?.name }}: {{ payload?.[0]?.value }}</div>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
</Tooltip>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Do not spread tooltip slot props with `v-bind="tooltipProps"`; the project's ESLint auto-fix removes that spread. Destructure the slot instead. Each tooltip payload entry exposes the original row as `payload`.
|
|
21
|
+
|
|
22
|
+
Common slot payloads:
|
|
23
|
+
|
|
24
|
+
- Tooltip `content`: `{ active, payload, label, coordinate, accessibilityLayer }`. Payload entries carry `name`, `value`, `color`, `unit`, `dataKey`, and the original row as `payload`.
|
|
25
|
+
- Line, Area, and Scatter `dot` / `activeDot`: `{ cx, cy, index, value, payload }` plus the series' presentation attributes. Render SVG at `cx` and `cy`.
|
|
26
|
+
|
|
27
|
+
## Templates and events
|
|
28
|
+
|
|
29
|
+
- Write `class`, not `className`.
|
|
30
|
+
- Write SVG attributes in kebab-case, such as `stroke-width` and `stroke-dasharray`.
|
|
31
|
+
- Use Vue events such as `@click`, not callback props such as `:on-click`.
|
|
32
|
+
- Item event callbacks receive `(item, index, event)`.
|
|
33
|
+
|
|
34
|
+
## Models
|
|
35
|
+
|
|
36
|
+
- `v-model:active-index` is `number | null`; `null` means no active item. Never substitute `-1`.
|
|
37
|
+
- `v-model:range` on `Brush` is `BrushStartEndIndex | null`; `null` selects all rows and replaces separate start/end models.
|
|
38
|
+
- `v-model:hidden` on `Legend` controls hidden series keys.
|
|
39
|
+
- A supplied model value is controlled state. An omitted model lets the component keep local state.
|
|
40
|
+
|
|
41
|
+
Only bind a model documented by the retrieved chart guide. Do not infer that every chart root supports every model.
|
|
42
|
+
|
|
43
|
+
## Responsive size
|
|
44
|
+
|
|
45
|
+
Charts are responsive by default. Set a numeric `height` and let the chart fill its container's width; pass numeric `width` and `height` for a fixed box.
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<LineChart
|
|
49
|
+
:data="rows"
|
|
50
|
+
:height="300"
|
|
51
|
+
>
|
|
52
|
+
<!-- axes and series -->
|
|
53
|
+
</LineChart>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Do not add `ResponsiveContainer`; it is deprecated and scheduled for removal in 2.0.
|
|
57
|
+
|
|
58
|
+
## Data keys and missing values
|
|
59
|
+
|
|
60
|
+
`data-key` accepts a field name, numeric index, or accessor function. Set `name` or `name-key` when an accessor function's series needs a readable label. `NaN` and `Infinity` are missing values, not finite coordinates.
|
|
61
|
+
|
|
62
|
+
Retrieve `typed-rows` when wrong keys should become compile-time errors.
|
|
63
|
+
|
|
64
|
+
## Theme and selectors
|
|
65
|
+
|
|
66
|
+
A series without an explicit color uses `--v-charts-series-1` through `--v-charts-series-8`. Override those variables to theme a dashboard instead of repeating colors on every series.
|
|
67
|
+
|
|
68
|
+
Library classes use the `v-charts-` prefix. Item marks expose `data-v-charts-item-index`; use these hooks for CSS and tests rather than Recharts class names.
|
|
69
|
+
|
|
70
|
+
## Related guides
|
|
71
|
+
|
|
72
|
+
`installation`, `typed-rows`, `ssr-testing`.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- a command-line report */
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
const root = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const packageJson = JSON.parse(readFileSync(join(root, '..', 'package.json'), 'utf8'))
|
|
9
|
+
|
|
10
|
+
const chartIds = [
|
|
11
|
+
'area-chart',
|
|
12
|
+
'bar-chart',
|
|
13
|
+
'bar-list',
|
|
14
|
+
'calendar-heatmap',
|
|
15
|
+
'cohort-chart',
|
|
16
|
+
'composed-chart',
|
|
17
|
+
'funnel-chart',
|
|
18
|
+
'heatmap',
|
|
19
|
+
'journey-sankey',
|
|
20
|
+
'line-chart',
|
|
21
|
+
'pie-chart',
|
|
22
|
+
'radar-chart',
|
|
23
|
+
'radial-bar-chart',
|
|
24
|
+
'sankey',
|
|
25
|
+
'scatter-chart',
|
|
26
|
+
'sparkline',
|
|
27
|
+
'sunburst-chart',
|
|
28
|
+
'tracker',
|
|
29
|
+
'treemap',
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
export function loadCatalog() {
|
|
33
|
+
const catalog = JSON.parse(readFileSync(join(root, 'catalog.json'), 'utf8'))
|
|
34
|
+
return { ...catalog, vccsVersion: packageJson.version }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalize(value) {
|
|
38
|
+
return value.toLowerCase().replaceAll('_', '-').trim()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stopwords = new Set(['a', 'an', 'and', 'are', 'build', 'by', 'chart', 'create', 'for', 'from', 'graph', 'how', 'in', 'is', 'make', 'need', 'of', 'on', 'plot', 'show', 'the', 'to', 'use', 'using', 'want', 'with'])
|
|
42
|
+
|
|
43
|
+
function queryTokens(query) {
|
|
44
|
+
return normalize(query).split(/[^\p{L}\p{N}]+/u)
|
|
45
|
+
.filter(token => token.length > 1 && !stopwords.has(token))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compact(value) {
|
|
49
|
+
return normalize(value).replace(/[^\p{L}\p{N}]/gu, '')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function summary(entry, score) {
|
|
53
|
+
return {
|
|
54
|
+
id: entry.id,
|
|
55
|
+
title: entry.title,
|
|
56
|
+
description: entry.description,
|
|
57
|
+
category: entry.category,
|
|
58
|
+
path: entry.path,
|
|
59
|
+
score,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function score(entry, query) {
|
|
64
|
+
const normalizedQuery = normalize(query)
|
|
65
|
+
if (!normalizedQuery)
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
const names = [entry.id, entry.title, ...entry.components]
|
|
69
|
+
const identity = normalize(names.join(' '))
|
|
70
|
+
const topics = normalize([entry.description, ...entry.useCases, ...entry.keywords].join(' '))
|
|
71
|
+
const exactName = names.some(name => compact(name) === compact(query))
|
|
72
|
+
let result = exactName ? 160 : identity.includes(normalizedQuery) ? 60 : 0
|
|
73
|
+
|
|
74
|
+
for (const token of queryTokens(query)) {
|
|
75
|
+
if (identity.includes(token))
|
|
76
|
+
result += 20
|
|
77
|
+
if (topics.includes(token))
|
|
78
|
+
result += 6
|
|
79
|
+
}
|
|
80
|
+
for (const phrase of [...entry.useCases, ...entry.keywords]) {
|
|
81
|
+
const normalizedPhrase = normalize(phrase)
|
|
82
|
+
if (normalizedPhrase.length > 1 && normalizedQuery.includes(normalizedPhrase))
|
|
83
|
+
result += 12
|
|
84
|
+
}
|
|
85
|
+
return result
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function searchCatalog(catalog, query, limit = 8) {
|
|
89
|
+
return catalog.guides
|
|
90
|
+
.map(entry => summary(entry, score(entry, query)))
|
|
91
|
+
.filter(entry => entry.score > 0)
|
|
92
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
93
|
+
.slice(0, limit)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function retrieveGuides(catalog, ids) {
|
|
97
|
+
return ids.map((id) => {
|
|
98
|
+
const entry = catalog.guides.find(guide => guide.id === id)
|
|
99
|
+
if (!entry)
|
|
100
|
+
throw new Error(`Unknown guide "${id}". Run the list command to see valid ids.`)
|
|
101
|
+
return readFileSync(join(root, entry.path), 'utf8')
|
|
102
|
+
}).join('\n\n---\n\n')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function validateCatalog(catalog) {
|
|
106
|
+
const errors = []
|
|
107
|
+
const seen = new Set()
|
|
108
|
+
for (const entry of catalog.guides) {
|
|
109
|
+
if (seen.has(entry.id))
|
|
110
|
+
errors.push(`Duplicate guide id: ${entry.id}`)
|
|
111
|
+
seen.add(entry.id)
|
|
112
|
+
for (const field of ['title', 'description', 'category', 'path']) {
|
|
113
|
+
if (!entry[field]) {
|
|
114
|
+
errors.push(`${entry.id}: missing ${field}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (entry.path.includes('..') || !entry.path.startsWith('guides/'))
|
|
118
|
+
errors.push(`${entry.id}: unsafe guide path ${entry.path}`)
|
|
119
|
+
if (!existsSync(join(root, entry.path))) {
|
|
120
|
+
errors.push(`${entry.id}: missing file ${entry.path}`)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (entry.category === 'chart') {
|
|
124
|
+
const content = readFileSync(join(root, entry.path), 'utf8')
|
|
125
|
+
for (const marker of ['## Data shape', '## Minimal example', '## Chart-specific rules', '```vue']) {
|
|
126
|
+
if (!content.includes(marker)) {
|
|
127
|
+
errors.push(`${entry.id}: missing ${marker}`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!content.includes(entry.components[0]))
|
|
131
|
+
errors.push(`${entry.id}: example does not name ${entry.components[0]}`)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const actualChartIds = catalog.guides.filter(entry => entry.category === 'chart').map(entry => entry.id).sort()
|
|
136
|
+
for (const id of chartIds) {
|
|
137
|
+
if (!actualChartIds.includes(id)) {
|
|
138
|
+
errors.push(`Missing chart guide: ${id}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const id of actualChartIds) {
|
|
142
|
+
if (!chartIds.includes(id)) {
|
|
143
|
+
errors.push(`Unexpected chart guide: ${id}`)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return errors
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function usage() {
|
|
150
|
+
return 'Usage: vccs-usage <search "query" | list | retrieve "id[,id...]" | validate>'
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function run(argv) {
|
|
154
|
+
const [command, argument] = argv
|
|
155
|
+
const catalog = loadCatalog()
|
|
156
|
+
if (command === 'search' && argument) {
|
|
157
|
+
const results = searchCatalog(catalog, argument)
|
|
158
|
+
if (!results.length)
|
|
159
|
+
throw new Error('No matching guides. Run the list command to browse all guides.')
|
|
160
|
+
console.log(JSON.stringify(results, null, 2))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
if (command === 'list' && !argument) {
|
|
164
|
+
console.log(JSON.stringify(catalog.guides.map(entry => summary(entry, undefined)), null, 2))
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
if (command === 'retrieve' && argument) {
|
|
168
|
+
const ids = argument.split(',').map(id => id.trim()).filter(Boolean)
|
|
169
|
+
process.stdout.write(retrieveGuides(catalog, ids))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
if (command === 'validate' && !argument) {
|
|
173
|
+
const errors = validateCatalog(catalog)
|
|
174
|
+
if (errors.length)
|
|
175
|
+
throw new Error(errors.join('\n'))
|
|
176
|
+
const chartCount = catalog.guides.filter(entry => entry.category === 'chart').length
|
|
177
|
+
console.log(`PASS: vccs ${catalog.vccsVersion}; ${catalog.guides.length} guides; ${chartCount} chart types; all chart guides have data and Vue examples.`)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
throw new Error(usage())
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
|
184
|
+
try {
|
|
185
|
+
run(process.argv.slice(2))
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.error(error.message)
|
|
189
|
+
process.exitCode = 1
|
|
190
|
+
}
|
|
191
|
+
}
|