@poveste/controls 0.6.0 → 0.7.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/package.json +16 -3
- package/postcss.config.cjs +0 -7
- package/src/components/HstCopyIcon.story.vue +0 -26
- package/src/components/HstCopyIcon.vue +0 -38
- package/src/components/HstWrapper.vue +0 -41
- package/src/components/button/HstButton.story.vue +0 -30
- package/src/components/button/HstButton.vue +0 -26
- package/src/components/button/HstButtonGroup.story.vue +0 -79
- package/src/components/button/HstButtonGroup.vue +0 -71
- package/src/components/checkbox/HstCheckbox.spec.ts +0 -28
- package/src/components/checkbox/HstCheckbox.story.vue +0 -47
- package/src/components/checkbox/HstCheckbox.vue +0 -56
- package/src/components/checkbox/HstCheckboxList.story.vue +0 -49
- package/src/components/checkbox/HstCheckboxList.vue +0 -82
- package/src/components/checkbox/HstSimpleCheckbox.story.vue +0 -28
- package/src/components/checkbox/HstSimpleCheckbox.vue +0 -82
- package/src/components/checkbox/__snapshots__/HstCheckbox.spec.ts.snap +0 -5
- package/src/components/colorselect/HstColorSelect.story.vue +0 -28
- package/src/components/colorselect/HstColorSelect.vue +0 -89
- package/src/components/design-tokens/HstColorShades.story.vue +0 -392
- package/src/components/design-tokens/HstColorShades.vue +0 -101
- package/src/components/design-tokens/HstTokenGrid.story.vue +0 -36
- package/src/components/design-tokens/HstTokenGrid.vue +0 -88
- package/src/components/design-tokens/HstTokenList.story.vue +0 -81
- package/src/components/design-tokens/HstTokenList.vue +0 -62
- package/src/components/json/HstJson.story.vue +0 -41
- package/src/components/json/HstJson.vue +0 -167
- package/src/components/json/serialize.spec.ts +0 -85
- package/src/components/json/serialize.ts +0 -54
- package/src/components/number/HstNumber.story.vue +0 -42
- package/src/components/number/HstNumber.vue +0 -95
- package/src/components/radio/HstRadio.story.vue +0 -51
- package/src/components/radio/HstRadio.vue +0 -109
- package/src/components/select/CustomSelect.vue +0 -126
- package/src/components/select/HstSelect.story.vue +0 -161
- package/src/components/select/HstSelect.vue +0 -40
- package/src/components/slider/HstSlider.story.vue +0 -37
- package/src/components/slider/HstSlider.vue +0 -106
- package/src/components/text/HstText.story.vue +0 -63
- package/src/components/text/HstText.vue +0 -44
- package/src/components/textarea/HstTextarea.story.vue +0 -35
- package/src/components/textarea/HstTextarea.vue +0 -44
- package/src/end.d.ts +0 -8
- package/src/index.ts +0 -54
- package/src/style/main.css +0 -65
- package/src/types.ts +0 -4
- package/src/utils.ts +0 -12
- package/tsconfig.json +0 -53
- package/vite.config.ts +0 -46
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import HstTokenGrid from './HstTokenGrid.vue'
|
|
3
|
-
|
|
4
|
-
const tokens = {
|
|
5
|
-
'sm': 'filter: drop-shadow(0 1px 1px rgb(0 0 0 / 0.05))',
|
|
6
|
-
'DEFAULT': 'filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.1)) drop-shadow(0 1px 1px rgb(0 0 0 / 0.06))',
|
|
7
|
-
'md': 'filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))',
|
|
8
|
-
'lg': 'filter: drop-shadow(0 10px 8px rgb(0 0 0 / 0.04)) drop-shadow(0 4px 3px rgb(0 0 0 / 0.1))',
|
|
9
|
-
'xl': 'filter: drop-shadow(0 20px 13px rgb(0 0 0 / 0.03)) drop-shadow(0 8px 5px rgb(0 0 0 / 0.08))',
|
|
10
|
-
'2xl': 'filter: drop-shadow(0 25px 25px rgb(0 0 0 / 0.15))',
|
|
11
|
-
'none': 'filter: drop-shadow(0 0 #0000)',
|
|
12
|
-
}
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<template>
|
|
16
|
-
<Story
|
|
17
|
-
title="HstTokenGrid"
|
|
18
|
-
group="design-system"
|
|
19
|
-
responsive-disabled
|
|
20
|
-
>
|
|
21
|
-
<Variant title="default">
|
|
22
|
-
<HstTokenGrid
|
|
23
|
-
:tokens="tokens"
|
|
24
|
-
:get-name="key => key === 'DEFAULT' ? 'drop-shadow' : `drop-shadow-${key}`"
|
|
25
|
-
:col-size="160"
|
|
26
|
-
>
|
|
27
|
-
<template #default="{ token }">
|
|
28
|
-
<div
|
|
29
|
-
class="w-32 h-32 bg-white dark:bg-black rounded mb-2"
|
|
30
|
-
:style="token.value"
|
|
31
|
-
/>
|
|
32
|
-
</template>
|
|
33
|
-
</HstTokenGrid>
|
|
34
|
-
</Variant>
|
|
35
|
-
</Story>
|
|
36
|
-
</template>
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export default {
|
|
3
|
-
name: 'HstTokenGrid',
|
|
4
|
-
}
|
|
5
|
-
</script>
|
|
6
|
-
|
|
7
|
-
<script lang="ts" setup>
|
|
8
|
-
import { VTooltip as vTooltip } from 'floating-vue'
|
|
9
|
-
import { computed, ref } from 'vue'
|
|
10
|
-
import HstCopyIcon from '../HstCopyIcon.vue'
|
|
11
|
-
|
|
12
|
-
const props = withDefaults(defineProps<{
|
|
13
|
-
tokens: Record<string, string | number | any[] | Record<string, any>>
|
|
14
|
-
colSize?: number
|
|
15
|
-
getName?: (key: string, value: string | number | any[] | Record<string, any>) => string
|
|
16
|
-
}>(), {
|
|
17
|
-
colSize: 180,
|
|
18
|
-
getName: null,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const processedTokens = computed(() => {
|
|
22
|
-
const list = props.tokens
|
|
23
|
-
const getName = props.getName
|
|
24
|
-
return Object.entries(list).map(([key, value]) => {
|
|
25
|
-
const name = getName ? getName(key, value) : key
|
|
26
|
-
return {
|
|
27
|
-
key,
|
|
28
|
-
name,
|
|
29
|
-
value: typeof value === 'number' ? value.toString() : value,
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const colSizePx = computed(() => `${props.colSize}px`)
|
|
35
|
-
|
|
36
|
-
const hover = ref<string>(null)
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<template>
|
|
40
|
-
<div
|
|
41
|
-
class="poveste-token-grid bind-col-size grid gap-4 m-4"
|
|
42
|
-
:style="{
|
|
43
|
-
'--poveste-col-size': colSizePx,
|
|
44
|
-
}"
|
|
45
|
-
>
|
|
46
|
-
<div
|
|
47
|
-
v-for="token of processedTokens"
|
|
48
|
-
:key="token.key"
|
|
49
|
-
class="flex flex-col gap-2"
|
|
50
|
-
@mouseenter="hover = token.key"
|
|
51
|
-
@mouseleave="hover = null"
|
|
52
|
-
>
|
|
53
|
-
<slot
|
|
54
|
-
:token="token"
|
|
55
|
-
/>
|
|
56
|
-
<div>
|
|
57
|
-
<div class="flex gap-1">
|
|
58
|
-
<pre
|
|
59
|
-
v-tooltip="token.name.length > colSize / 8 ? token.name : ''"
|
|
60
|
-
class="my-0 truncate shrink"
|
|
61
|
-
>{{ token.name }}</pre>
|
|
62
|
-
<HstCopyIcon
|
|
63
|
-
v-if="hover === token.key"
|
|
64
|
-
:content="token.name"
|
|
65
|
-
class="flex-none"
|
|
66
|
-
/>
|
|
67
|
-
</div>
|
|
68
|
-
<div class="flex gap-1">
|
|
69
|
-
<pre
|
|
70
|
-
v-tooltip="token.value.length > colSize / 8 ? token.value : ''"
|
|
71
|
-
class="my-0 opacity-50 truncate shrink"
|
|
72
|
-
>{{ token.value }}</pre>
|
|
73
|
-
<HstCopyIcon
|
|
74
|
-
v-if="hover === token.key"
|
|
75
|
-
:content="typeof token.value === 'string' ? token.value : JSON.stringify(token.value)"
|
|
76
|
-
class="flex-none"
|
|
77
|
-
/>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
</template>
|
|
83
|
-
|
|
84
|
-
<style>
|
|
85
|
-
.bind-col-size {
|
|
86
|
-
grid-template-columns: repeat(auto-fill, minmax(var(--poveste-col-size), 1fr));
|
|
87
|
-
}
|
|
88
|
-
</style>
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import type { CSSProperties } from 'vue'
|
|
3
|
-
import HstTokenList from './HstTokenList.vue'
|
|
4
|
-
|
|
5
|
-
const tokens = {
|
|
6
|
-
'0': 0,
|
|
7
|
-
'1': '0.25rem',
|
|
8
|
-
'2': '0.5rem',
|
|
9
|
-
'3': '0.75rem',
|
|
10
|
-
'4': '1rem',
|
|
11
|
-
'5': '1.25rem',
|
|
12
|
-
'6': '1.5rem',
|
|
13
|
-
'8': '2rem',
|
|
14
|
-
'10': '2.5rem',
|
|
15
|
-
'12': '3rem',
|
|
16
|
-
'16': '4rem',
|
|
17
|
-
'20': '5rem',
|
|
18
|
-
'24': '6rem',
|
|
19
|
-
'32': '8rem',
|
|
20
|
-
'40': '10rem',
|
|
21
|
-
'48': '12rem',
|
|
22
|
-
'56': '14rem',
|
|
23
|
-
'64': '16rem',
|
|
24
|
-
'auto': 'auto',
|
|
25
|
-
'px': '1px',
|
|
26
|
-
'screen': '100vw',
|
|
27
|
-
'1/2': '50%',
|
|
28
|
-
'1/3': '33.333333%',
|
|
29
|
-
'2/3': '66.666667%',
|
|
30
|
-
'1/4': '25%',
|
|
31
|
-
'2/4': '50%',
|
|
32
|
-
'3/4': '75%',
|
|
33
|
-
'1/5': '20%',
|
|
34
|
-
'2/5': '40%',
|
|
35
|
-
'3/5': '60%',
|
|
36
|
-
'4/5': '80%',
|
|
37
|
-
'1/6': '16.666667%',
|
|
38
|
-
'2/6': '33.333333%',
|
|
39
|
-
'3/6': '50%',
|
|
40
|
-
'4/6': '66.666667%',
|
|
41
|
-
'5/6': '83.333333%',
|
|
42
|
-
'1/12': '8.333333%',
|
|
43
|
-
'2/12': '16.666667%',
|
|
44
|
-
'3/12': '25%',
|
|
45
|
-
'4/12': '33.333333%',
|
|
46
|
-
'5/12': '41.666667%',
|
|
47
|
-
'6/12': '50%',
|
|
48
|
-
'7/12': '58.333333%',
|
|
49
|
-
'8/12': '66.666667%',
|
|
50
|
-
'9/12': '75%',
|
|
51
|
-
'10/12': '83.333333%',
|
|
52
|
-
'11/12': '91.666667%',
|
|
53
|
-
'full': '100%',
|
|
54
|
-
}
|
|
55
|
-
</script>
|
|
56
|
-
|
|
57
|
-
<template>
|
|
58
|
-
<Story
|
|
59
|
-
title="HstTokenList"
|
|
60
|
-
group="design-system"
|
|
61
|
-
responsive-disabled
|
|
62
|
-
>
|
|
63
|
-
<Variant title="default">
|
|
64
|
-
<HstTokenList
|
|
65
|
-
:tokens="tokens"
|
|
66
|
-
:get-name="key => `w-${key}`"
|
|
67
|
-
>
|
|
68
|
-
<template #default="{ token }">
|
|
69
|
-
<div class="bg-gray-500/10">
|
|
70
|
-
<div
|
|
71
|
-
class="h-20 bg-gray-500/50"
|
|
72
|
-
:style="{
|
|
73
|
-
width: token.value as string,
|
|
74
|
-
} as CSSProperties"
|
|
75
|
-
/>
|
|
76
|
-
</div>
|
|
77
|
-
</template>
|
|
78
|
-
</HstTokenList>
|
|
79
|
-
</Variant>
|
|
80
|
-
</Story>
|
|
81
|
-
</template>
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export default {
|
|
3
|
-
name: 'HstTokenList',
|
|
4
|
-
}
|
|
5
|
-
</script>
|
|
6
|
-
|
|
7
|
-
<script lang="ts" setup>
|
|
8
|
-
import { computed, ref } from 'vue'
|
|
9
|
-
import HstCopyIcon from '../HstCopyIcon.vue'
|
|
10
|
-
|
|
11
|
-
const props = defineProps<{
|
|
12
|
-
tokens: Record<string, string | number | any[] | Record<string, any>>
|
|
13
|
-
getName?: (key: string, value: string | number | any[] | Record<string, any>) => string
|
|
14
|
-
}>()
|
|
15
|
-
|
|
16
|
-
const processedTokens = computed(() => {
|
|
17
|
-
const list = props.tokens
|
|
18
|
-
const getName = props.getName
|
|
19
|
-
return Object.entries(list).map(([key, value]) => {
|
|
20
|
-
const name = getName ? getName(key, value) : key
|
|
21
|
-
return {
|
|
22
|
-
key,
|
|
23
|
-
name,
|
|
24
|
-
value: typeof value === 'number' ? value.toString() : value,
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
const hover = ref<string>(null)
|
|
30
|
-
</script>
|
|
31
|
-
|
|
32
|
-
<template>
|
|
33
|
-
<div
|
|
34
|
-
v-for="token of processedTokens"
|
|
35
|
-
:key="token.key"
|
|
36
|
-
class="poveste-token-list flex flex-col gap-2 my-8"
|
|
37
|
-
@mouseenter="hover = token.key"
|
|
38
|
-
@mouseleave="hover = null"
|
|
39
|
-
>
|
|
40
|
-
<slot
|
|
41
|
-
:token="token"
|
|
42
|
-
/>
|
|
43
|
-
<div class="mx-4">
|
|
44
|
-
<div class="flex gap-1">
|
|
45
|
-
<pre class="my-0 truncate shrink">{{ token.name }}</pre>
|
|
46
|
-
<HstCopyIcon
|
|
47
|
-
v-if="hover === token.key"
|
|
48
|
-
:content="token.name"
|
|
49
|
-
class="flex-none"
|
|
50
|
-
/>
|
|
51
|
-
</div>
|
|
52
|
-
<div class="flex gap-1">
|
|
53
|
-
<pre class="my-0 opacity-50 truncate shrink">{{ token.value }}</pre>
|
|
54
|
-
<HstCopyIcon
|
|
55
|
-
v-if="hover === token.key"
|
|
56
|
-
:content="typeof token.value === 'string' ? token.value : JSON.stringify(token.value)"
|
|
57
|
-
class="flex-none"
|
|
58
|
-
/>
|
|
59
|
-
</div>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
</template>
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import HstJson from './HstJson.vue'
|
|
3
|
-
|
|
4
|
-
function initState() {
|
|
5
|
-
return {
|
|
6
|
-
film: {
|
|
7
|
-
year: 2017,
|
|
8
|
-
title: 'Blade Runner 2049',
|
|
9
|
-
actors: ['Ryan Gosling', 'Harrison Ford', 'Ana de Armas', 'Sylvia Hoeks'],
|
|
10
|
-
},
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
</script>
|
|
14
|
-
|
|
15
|
-
<template>
|
|
16
|
-
<Story
|
|
17
|
-
title="HstJson"
|
|
18
|
-
group="controls"
|
|
19
|
-
:layout="{ type: 'single', iframe: false }"
|
|
20
|
-
>
|
|
21
|
-
<Variant
|
|
22
|
-
title="default"
|
|
23
|
-
:init-state="initState"
|
|
24
|
-
>
|
|
25
|
-
<template #default="{ state }">
|
|
26
|
-
<HstJson
|
|
27
|
-
v-model="state.film"
|
|
28
|
-
title="Textarea"
|
|
29
|
-
/>
|
|
30
|
-
<pre>{{ state.film }}</pre>
|
|
31
|
-
</template>
|
|
32
|
-
|
|
33
|
-
<template #controls="{ state }">
|
|
34
|
-
<HstJson
|
|
35
|
-
v-model="state.film"
|
|
36
|
-
title="Text"
|
|
37
|
-
/>
|
|
38
|
-
</template>
|
|
39
|
-
</Variant>
|
|
40
|
-
</Story>
|
|
41
|
-
</template>
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
export default {
|
|
3
|
-
name: 'HstJson',
|
|
4
|
-
inheritAttrs: false,
|
|
5
|
-
}
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<script lang="ts" setup>
|
|
9
|
-
import type {
|
|
10
|
-
ViewUpdate,
|
|
11
|
-
} from '@codemirror/view'
|
|
12
|
-
import { defaultKeymap } from '@codemirror/commands'
|
|
13
|
-
import { json } from '@codemirror/lang-json'
|
|
14
|
-
import {
|
|
15
|
-
bracketMatching,
|
|
16
|
-
defaultHighlightStyle,
|
|
17
|
-
foldGutter,
|
|
18
|
-
foldKeymap,
|
|
19
|
-
indentOnInput,
|
|
20
|
-
syntaxHighlighting,
|
|
21
|
-
} from '@codemirror/language'
|
|
22
|
-
import { lintKeymap } from '@codemirror/lint'
|
|
23
|
-
import { Compartment } from '@codemirror/state'
|
|
24
|
-
import { oneDarkHighlightStyle, oneDarkTheme } from '@codemirror/theme-one-dark'
|
|
25
|
-
import {
|
|
26
|
-
EditorView,
|
|
27
|
-
highlightActiveLine,
|
|
28
|
-
highlightActiveLineGutter,
|
|
29
|
-
highlightSpecialChars,
|
|
30
|
-
keymap,
|
|
31
|
-
} from '@codemirror/view'
|
|
32
|
-
import { Icon } from '@iconify/vue'
|
|
33
|
-
import { VTooltip as vTooltip } from 'floating-vue'
|
|
34
|
-
import { onMounted, ref, watch, watchEffect } from 'vue'
|
|
35
|
-
import { isDark } from '../../utils'
|
|
36
|
-
import HstWrapper from '../HstWrapper.vue'
|
|
37
|
-
import { stringifyState } from './serialize.js'
|
|
38
|
-
|
|
39
|
-
const props = defineProps<{
|
|
40
|
-
title?: string
|
|
41
|
-
modelValue: unknown
|
|
42
|
-
}>()
|
|
43
|
-
|
|
44
|
-
const emit = defineEmits({
|
|
45
|
-
'update:modelValue': (newValue: unknown) => true,
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
let editorView: EditorView
|
|
49
|
-
// The document this control last rendered from `modelValue`. A change producing
|
|
50
|
-
// exactly this is our own render, not an edit, and must not be written back:
|
|
51
|
-
// `stringifyState` substitutes markers for what JSON cannot carry, so echoing
|
|
52
|
-
// our own document replaces a cycle in the story's state with the string
|
|
53
|
-
// `[Circular]`, and a function with `[Function]`.
|
|
54
|
-
let renderedDoc: string | undefined
|
|
55
|
-
const internalValue = ref('')
|
|
56
|
-
const invalidValue = ref(false)
|
|
57
|
-
const editorElement = ref<HTMLInputElement>()
|
|
58
|
-
|
|
59
|
-
const themes = {
|
|
60
|
-
light: [EditorView.baseTheme({}), syntaxHighlighting(defaultHighlightStyle)],
|
|
61
|
-
dark: [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)],
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const themeConfig = new Compartment()
|
|
65
|
-
|
|
66
|
-
const extensions = [
|
|
67
|
-
highlightActiveLineGutter(),
|
|
68
|
-
highlightActiveLine(),
|
|
69
|
-
highlightSpecialChars(),
|
|
70
|
-
json(),
|
|
71
|
-
bracketMatching(),
|
|
72
|
-
indentOnInput(),
|
|
73
|
-
foldGutter(),
|
|
74
|
-
keymap.of([
|
|
75
|
-
...defaultKeymap,
|
|
76
|
-
...foldKeymap,
|
|
77
|
-
...lintKeymap,
|
|
78
|
-
]),
|
|
79
|
-
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
|
80
|
-
internalValue.value = viewUpdate.view.state.doc.toString()
|
|
81
|
-
}),
|
|
82
|
-
themeConfig.of(themes.light),
|
|
83
|
-
]
|
|
84
|
-
|
|
85
|
-
onMounted(() => {
|
|
86
|
-
renderedDoc = stringifyState(props.modelValue, 2)
|
|
87
|
-
|
|
88
|
-
editorView = new EditorView({
|
|
89
|
-
doc: renderedDoc,
|
|
90
|
-
extensions,
|
|
91
|
-
parent: editorElement.value,
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
watchEffect(() => {
|
|
95
|
-
editorView.dispatch({
|
|
96
|
-
effects: [
|
|
97
|
-
themeConfig.reconfigure(themes[isDark.value ? 'dark' : 'light']),
|
|
98
|
-
],
|
|
99
|
-
})
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
watch(() => props.modelValue, () => {
|
|
104
|
-
let sameDocument
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
sameDocument = (stringifyState(JSON.parse(internalValue.value)) === stringifyState(props.modelValue))
|
|
108
|
-
}
|
|
109
|
-
catch (e) {
|
|
110
|
-
sameDocument = false
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!sameDocument) {
|
|
114
|
-
renderedDoc = stringifyState(props.modelValue, 2)
|
|
115
|
-
editorView.dispatch({ changes: [{ from: 0, to: editorView.state.doc.length, insert: renderedDoc }] })
|
|
116
|
-
}
|
|
117
|
-
}, { deep: true })
|
|
118
|
-
|
|
119
|
-
watch(() => internalValue.value, () => {
|
|
120
|
-
// Our own render coming back round. The model already holds this, and parsing
|
|
121
|
-
// it would hand back the markers instead of the values they stand for.
|
|
122
|
-
if (internalValue.value === renderedDoc) {
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
invalidValue.value = false
|
|
127
|
-
try {
|
|
128
|
-
emit('update:modelValue', JSON.parse(internalValue.value))
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
invalidValue.value = true
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
</script>
|
|
135
|
-
|
|
136
|
-
<template>
|
|
137
|
-
<HstWrapper
|
|
138
|
-
:title="title"
|
|
139
|
-
class="poveste-json cursor-text"
|
|
140
|
-
:class="$attrs.class"
|
|
141
|
-
:style="$attrs.style"
|
|
142
|
-
>
|
|
143
|
-
<div
|
|
144
|
-
ref="editorElement"
|
|
145
|
-
class="__poveste-json-code w-full border border-solid border-black/25 dark:border-white/25 focus-within:border-primary-500 dark:focus-within:border-primary-500 rounded-sm box-border overflow-auto resize-y min-h-32 h-48 relative"
|
|
146
|
-
v-bind="{ ...$attrs, class: null, style: null }"
|
|
147
|
-
/>
|
|
148
|
-
|
|
149
|
-
<template #actions>
|
|
150
|
-
<Icon
|
|
151
|
-
v-if="invalidValue"
|
|
152
|
-
v-tooltip="'JSON error'"
|
|
153
|
-
icon="carbon:warning-alt"
|
|
154
|
-
class="text-orange-500"
|
|
155
|
-
/>
|
|
156
|
-
|
|
157
|
-
<slot name="actions" />
|
|
158
|
-
</template>
|
|
159
|
-
</HstWrapper>
|
|
160
|
-
</template>
|
|
161
|
-
|
|
162
|
-
<style scoped>
|
|
163
|
-
.__poveste-json-code :deep(.cm-editor) {
|
|
164
|
-
height: 100%;
|
|
165
|
-
min-width: 280px;
|
|
166
|
-
}
|
|
167
|
-
</style>
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { EditorView } from '@codemirror/view'
|
|
2
|
-
import { mount } from '@vue/test-utils'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
import { nextTick } from 'vue'
|
|
5
|
-
import HstJson from './HstJson.vue'
|
|
6
|
-
import { stringifyState } from './serialize.js'
|
|
7
|
-
|
|
8
|
-
describe('stringifyState', () => {
|
|
9
|
-
it('matches JSON.stringify on values JSON can express', () => {
|
|
10
|
-
const value = { a: 1, b: 'two', c: [1, 2], d: { e: null }, f: true }
|
|
11
|
-
expect(stringifyState(value)).toBe(JSON.stringify(value))
|
|
12
|
-
expect(stringifyState(value, 2)).toBe(JSON.stringify(value, null, 2))
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
it('names a cycle instead of throwing', () => {
|
|
16
|
-
// The ComplexParameter story in the vue3 example holds exactly this, and
|
|
17
|
-
// once the state sync started delivering the story's own scope it reached
|
|
18
|
-
// the editor. `JSON.stringify` throws here, from a lifecycle hook, taking
|
|
19
|
-
// the rest of the flush — the panel and the toolbar — down with it.
|
|
20
|
-
const parent: any = { name: 'hello' }
|
|
21
|
-
const child: any = { parent }
|
|
22
|
-
parent.child = child
|
|
23
|
-
|
|
24
|
-
expect(JSON.parse(stringifyState(parent))).toEqual({
|
|
25
|
-
name: 'hello',
|
|
26
|
-
child: { parent: '[Circular]' },
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('names a function, which JSON.stringify drops silently', () => {
|
|
31
|
-
expect(JSON.parse(stringifyState({ action: () => {} }))).toEqual({ action: '[Function]' })
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('does not mistake a shared reference for a cycle', () => {
|
|
35
|
-
// The guard tracks ancestors, not everything seen. The same object twice as
|
|
36
|
-
// a sibling serialises fine and must not be blanked out.
|
|
37
|
-
const shared = { a: 1 }
|
|
38
|
-
expect(JSON.parse(stringifyState({ x: shared, y: shared }))).toEqual({ x: { a: 1 }, y: { a: 1 } })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('names a node rather than walking it', () => {
|
|
42
|
-
const el = document.createElement('div')
|
|
43
|
-
expect(JSON.parse(stringifyState({ el }))).toEqual({ el: '[Node]' })
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('carries a bigint across as its digits', () => {
|
|
47
|
-
expect(JSON.parse(stringifyState({ n: 10n }))).toEqual({ n: '10' })
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('hstJson round-trip', () => {
|
|
52
|
-
it('does not write its own markers back into the model', async () => {
|
|
53
|
-
// The markers stand for what JSON cannot carry. Parsing the rendered
|
|
54
|
-
// document and emitting it replaces the thing itself with its label — a
|
|
55
|
-
// cycle in the story's state becomes the string `[Circular]` — and it used
|
|
56
|
-
// to happen on mount, with nobody touching the editor.
|
|
57
|
-
const parent: any = { name: 'hello' }
|
|
58
|
-
parent.child = { parent }
|
|
59
|
-
|
|
60
|
-
const wrapper = mount(HstJson, { props: { modelValue: parent, title: 'Recursive' } })
|
|
61
|
-
await nextTick()
|
|
62
|
-
|
|
63
|
-
await wrapper.setProps({ modelValue: { ...parent, name: 'changed' } })
|
|
64
|
-
await nextTick()
|
|
65
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
66
|
-
|
|
67
|
-
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('still reports a document the reader changed', async () => {
|
|
71
|
-
// The guard is keyed on the exact text this control last rendered, so it
|
|
72
|
-
// must not swallow anything else. Without this, suppressing the echo would
|
|
73
|
-
// quietly turn the editor read-only and the test above would still pass.
|
|
74
|
-
const wrapper = mount(HstJson, { props: { modelValue: { a: 1 }, title: 'Plain' } })
|
|
75
|
-
await nextTick()
|
|
76
|
-
|
|
77
|
-
const view = EditorView.findFromDOM(wrapper.find('.cm-content').element as HTMLElement)!
|
|
78
|
-
expect(view).toBeTruthy()
|
|
79
|
-
|
|
80
|
-
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: '{ "a": 2 }' } })
|
|
81
|
-
await nextTick()
|
|
82
|
-
|
|
83
|
-
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{ a: 2 }])
|
|
84
|
-
})
|
|
85
|
-
})
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `JSON.stringify` for values that were never promised to be JSON.
|
|
3
|
-
*
|
|
4
|
-
* State reaches this control straight out of a story's own scope, so it holds
|
|
5
|
-
* whatever a `<script setup>` holds: a function, a DOM node, an object that
|
|
6
|
-
* refers back to itself. `JSON.stringify` throws on the cycle, and it throws
|
|
7
|
-
* from inside a lifecycle hook — which takes the rest of that flush with it, so
|
|
8
|
-
* the panel and the toolbar beside it never mount. A marker in the editor is
|
|
9
|
-
* the smaller loss, and it is what the story events pane already does.
|
|
10
|
-
*
|
|
11
|
-
* Nodes and windows are named rather than walked. Both are cyclic, so the guard
|
|
12
|
-
* below would terminate on them, but only after walking a few thousand
|
|
13
|
-
* properties of `window` on every keystroke.
|
|
14
|
-
*/
|
|
15
|
-
export function stringifyState(value: unknown, space?: number) {
|
|
16
|
-
// Ancestors, not everything seen: the same object appearing twice as a
|
|
17
|
-
// sibling is a shared reference, not a cycle, and is perfectly serialisable.
|
|
18
|
-
// The replacer's `this` is the holder, which is what lets the stack unwind.
|
|
19
|
-
const ancestors: any[] = []
|
|
20
|
-
|
|
21
|
-
return JSON.stringify(value, function (this: any, _key, current) {
|
|
22
|
-
if (typeof current === 'function') {
|
|
23
|
-
return '[Function]'
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (typeof current === 'bigint') {
|
|
27
|
-
return current.toString()
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (current === null || typeof current !== 'object') {
|
|
31
|
-
return current
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (typeof Node !== 'undefined' && current instanceof Node) {
|
|
35
|
-
return '[Node]'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof Window !== 'undefined' && current instanceof Window) {
|
|
39
|
-
return '[Window]'
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
|
|
43
|
-
ancestors.pop()
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (ancestors.includes(current)) {
|
|
47
|
-
return '[Circular]'
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
ancestors.push(current)
|
|
51
|
-
|
|
52
|
-
return current
|
|
53
|
-
}, space)
|
|
54
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
import HstNumber from './HstNumber.vue'
|
|
3
|
-
|
|
4
|
-
function initState() {
|
|
5
|
-
return {
|
|
6
|
-
count: 20,
|
|
7
|
-
step: 1,
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<template>
|
|
13
|
-
<Story
|
|
14
|
-
title="HstNumber"
|
|
15
|
-
group="controls"
|
|
16
|
-
>
|
|
17
|
-
<Variant
|
|
18
|
-
title="default"
|
|
19
|
-
:init-state="initState"
|
|
20
|
-
>
|
|
21
|
-
<template #default="{ state }">
|
|
22
|
-
<HstNumber
|
|
23
|
-
v-model="state.count"
|
|
24
|
-
:step="state.step"
|
|
25
|
-
title="Count"
|
|
26
|
-
/>
|
|
27
|
-
</template>
|
|
28
|
-
|
|
29
|
-
<template #controls="{ state }">
|
|
30
|
-
<HstNumber
|
|
31
|
-
v-model="state.count"
|
|
32
|
-
title="Count"
|
|
33
|
-
/>
|
|
34
|
-
|
|
35
|
-
<HstNumber
|
|
36
|
-
v-model="state.step"
|
|
37
|
-
title="Step"
|
|
38
|
-
/>
|
|
39
|
-
</template>
|
|
40
|
-
</Variant>
|
|
41
|
-
</Story>
|
|
42
|
-
</template>
|