@poveste/app 0.15.0 → 0.16.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/dist/app/style/main.pcss +18 -44
- package/dist/bundled/App.vue_vue_type_script_setup_true_lang.js +9 -7
- package/dist/bundled/app.css +80 -17
- package/dist/bundled/components/app/AppActions.vue_vue_type_script_setup_true_lang.js +44 -27
- package/dist/bundled/components/app/ShortcutTooltip.js +5 -0
- package/dist/bundled/components/app/ShortcutTooltip.vue_vue_type_script_setup_true_lang.js +27 -0
- package/dist/bundled/components/base/BaseDropdown.js +6 -0
- package/dist/bundled/components/base/BaseDropdown.vue_vue_type_script_setup_true_lang.js +68 -0
- package/dist/bundled/components/base/BaseOverflowMenu.vue_vue_type_script_setup_true_lang.js +5 -5
- package/dist/bundled/components/base/BaseSelect.js +1 -0
- package/dist/bundled/components/base/BaseSelect.vue_vue_type_script_setup_true_lang.js +80 -51
- package/dist/bundled/components/base/BaseSplitPane.js +1 -1
- package/dist/bundled/components/base/BaseSplitPane.vue_vue_type_script_setup_true_lang.js +65 -4
- package/dist/bundled/components/panel/ControlsComponentPropItem.vue_vue_type_script_setup_true_lang.js +15 -13
- package/dist/bundled/components/panel/ControlsComponentProps.vue_vue_type_script_setup_true_lang.js +9 -6
- package/dist/bundled/components/panel/ControlsComponentState.vue_vue_type_script_setup_true_lang.js +9 -6
- package/dist/bundled/components/panel/StatePresets.vue_vue_type_script_setup_true_lang.js +69 -54
- package/dist/bundled/components/panel/StoryEvent.vue_vue_type_script_setup_true_lang.js +5 -5
- package/dist/bundled/components/panel/StorySidePanel.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/bundled/components/panel/StorySourceCode.js +1 -1
- package/dist/bundled/components/panel/StorySourceCode.vue_vue_type_script_setup_true_lang.js +21 -16
- package/dist/bundled/components/story/StoryResponsivePreview.js +1 -1
- package/dist/bundled/components/story/StoryResponsivePreview.vue_vue_type_script_setup_true_lang.js +46 -43
- package/dist/bundled/components/story/StoryVariantGridItem.js +1 -1
- package/dist/bundled/components/story/StoryVariantGridItem.vue_vue_type_script_setup_true_lang.js +21 -19
- package/dist/bundled/components/story/StoryVariantSingle.js +1 -1
- package/dist/bundled/components/story/StoryVariantSingle.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/bundled/components/story/StoryView.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/bundled/components/toolbar/DevOnlyToolbarOpenInEditor.vue_vue_type_script_setup_true_lang.js +15 -12
- package/dist/bundled/components/toolbar/ToolbarBackground.vue_vue_type_script_setup_true_lang.js +20 -16
- package/dist/bundled/components/toolbar/ToolbarNewTab.vue_vue_type_script_setup_true_lang.js +16 -13
- package/dist/bundled/components/toolbar/ToolbarResponsiveSize.vue_vue_type_script_setup_true_lang.js +35 -24
- package/dist/bundled/components/toolbar/ToolbarTextDirection.vue_vue_type_script_setup_true_lang.js +15 -12
- package/dist/bundled/index.js +0 -13
- package/dist/bundled/sandbox.js +2 -1
- package/dist/bundled/util/icons.generated.js +4 -2
- package/dist/bundled/util/scroll.js +26 -5
- package/dist/bundled/util/state.js +105 -5
- package/dist/style.css +731 -357
- package/package.json +19 -15
- package/src/__tests__/base-select.spec.ts +25 -0
- package/src/__tests__/reset.spec.ts +53 -0
- package/src/__tests__/state-clone.spec.ts +55 -1
- package/src/__tests__/state-types.spec.ts +175 -0
- package/src/app/App.vue +43 -34
- package/src/app/components/app/AppActions.vue +42 -30
- package/src/app/components/app/ShortcutTooltip.vue +29 -0
- package/src/app/components/base/BaseDropdown.vue +142 -0
- package/src/app/components/base/BaseOverflowMenu.vue +10 -5
- package/src/app/components/base/BaseSelect.vue +133 -60
- package/src/app/components/base/BaseSplitPane.vue +73 -2
- package/src/app/components/panel/ControlsComponentPropItem.vue +18 -17
- package/src/app/components/panel/ControlsComponentProps.vue +7 -5
- package/src/app/components/panel/ControlsComponentState.vue +7 -5
- package/src/app/components/panel/StatePresets.vue +82 -76
- package/src/app/components/panel/StoryEvent.vue +10 -9
- package/src/app/components/panel/StorySidePanel.vue +1 -0
- package/src/app/components/panel/StorySourceCode.vue +47 -41
- package/src/app/components/story/StoryResponsivePreview.vue +55 -27
- package/src/app/components/story/StoryVariantGridItem.vue +19 -18
- package/src/app/components/story/StoryVariantSingle.vue +1 -0
- package/src/app/components/story/StoryView.vue +1 -0
- package/src/app/components/toolbar/DevOnlyToolbarOpenInEditor.vue +14 -12
- package/src/app/components/toolbar/ToolbarBackground.vue +22 -23
- package/src/app/components/toolbar/ToolbarNewTab.vue +14 -12
- package/src/app/components/toolbar/ToolbarResponsiveSize.vue +31 -25
- package/src/app/components/toolbar/ToolbarTextDirection.vue +14 -10
- package/src/app/index.ts +0 -17
- package/src/app/sandbox.ts +8 -1
- package/src/app/style/main.pcss +18 -44
- package/src/app/util/icons.generated.ts +2 -2
- package/src/app/util/scroll.ts +47 -9
- package/src/app/util/state.ts +139 -6
- package/dist/app/style/floating.pcss +0 -40
- package/dist/bundled/util/tooltip.js +0 -14
- package/src/app/style/floating.pcss +0 -40
- package/src/app/util/tooltip.ts +0 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poveste/app",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.16.1",
|
|
5
5
|
"description": "Pre-bundled UI for Poveste",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sorin Gitlan",
|
|
@@ -31,39 +31,43 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": "
|
|
34
|
+
"node": ">=22.22.2"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@poveste/controls": "^0.
|
|
38
|
-
"@poveste/shared": "^0.
|
|
39
|
-
"@poveste/vendors": "^0.
|
|
37
|
+
"@poveste/controls": "^0.16.1",
|
|
38
|
+
"@poveste/shared": "^0.16.1",
|
|
39
|
+
"@poveste/vendors": "^0.16.1",
|
|
40
40
|
"fuse.js": "^7.0.0",
|
|
41
41
|
"shiki": "^4.4.3"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@fontsource/noto-sans-display": "^5.3.0",
|
|
45
|
-
"@iconify-json/carbon": "^1.2.
|
|
46
|
-
"@iconify-json/fluent": "1.2.
|
|
45
|
+
"@iconify-json/carbon": "^1.2.27",
|
|
46
|
+
"@iconify-json/fluent": "1.2.58",
|
|
47
47
|
"@iconify-json/mdi": "^1.2.3",
|
|
48
48
|
"@iconify-json/ri": "^1.2.10",
|
|
49
|
-
"@iconify/utils": "^3.1.
|
|
49
|
+
"@iconify/utils": "^3.1.7",
|
|
50
|
+
"@iconify/vue": "^5.0.3",
|
|
50
51
|
"@tailwindcss/postcss": "^4.3.3",
|
|
51
|
-
"@tailwindcss/typography": "^0.5.
|
|
52
|
+
"@tailwindcss/typography": "^0.5.20",
|
|
52
53
|
"@types/fs-extra": "^11.0.4",
|
|
53
|
-
"@types/node": "^
|
|
54
|
-
"@vitejs/plugin-vue": "^6.0.
|
|
54
|
+
"@types/node": "^26.6.3",
|
|
55
|
+
"@vitejs/plugin-vue": "^6.0.9",
|
|
56
|
+
"@vueuse/core": "^15.0.0",
|
|
55
57
|
"chokidar": "^5.0.0",
|
|
56
58
|
"concurrently": "^10.0.5",
|
|
57
|
-
"fs-extra": "^11.
|
|
59
|
+
"fs-extra": "^11.4.1",
|
|
58
60
|
"globby": "^16.2.4",
|
|
59
61
|
"pathe": "^2.0.3",
|
|
62
|
+
"pinia": "^4.0.3",
|
|
60
63
|
"postcss": "^8.5.23",
|
|
61
64
|
"postcss-cli": "^12.0.0",
|
|
62
65
|
"tailwindcss": "^4.3.3",
|
|
63
66
|
"typescript": "6.0.3",
|
|
64
|
-
"vite": "^8.
|
|
65
|
-
"vitest": "^5.0.
|
|
66
|
-
"vue": "^3.5.
|
|
67
|
+
"vite": "^8.3.1",
|
|
68
|
+
"vitest": "^5.0.2",
|
|
69
|
+
"vue": "^3.5.43",
|
|
70
|
+
"vue-router": "^5.3.1",
|
|
67
71
|
"vue-tsc": "^3.3.11"
|
|
68
72
|
},
|
|
69
73
|
"scripts": {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* The same defect as `@poveste/controls`' own select, in the chrome's copy: the
|
|
7
|
+
* options are portalled to `body`, which carries no colour, so a background
|
|
8
|
+
* without a text colour left them at the UA default — black on the dark panel.
|
|
9
|
+
*
|
|
10
|
+
* `__dirname` rather than `import.meta.url`, which vitest's transform does not
|
|
11
|
+
* leave intact here — the trap `icons.spec.ts` records.
|
|
12
|
+
*/
|
|
13
|
+
const source = readFileSync(resolve(__dirname, '../app/components/base/BaseSelect.vue'), 'utf8')
|
|
14
|
+
|
|
15
|
+
describe('the chrome select options', () => {
|
|
16
|
+
const classes = /class="poveste-base-select-options([^"]*)"/.exec(source)?.[1] ?? ''
|
|
17
|
+
|
|
18
|
+
it('carries a text colour, not only a background', () => {
|
|
19
|
+
expect(classes).toMatch(/\btext-gray-\d+\b/)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('carries one for dark as well', () => {
|
|
23
|
+
expect(classes).toMatch(/\bdark:text-gray-\d+\b/)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Preflight is deliberately off and `main.pcss` replaces it by hand, so anything
|
|
7
|
+
* the replacement leaves out is a UA default the chrome then renders. The one
|
|
8
|
+
* that shipped was the button background: `ButtonFace` reads as a light grey
|
|
9
|
+
* panel, close enough to the light chrome to pass review and a white hole in dark.
|
|
10
|
+
*
|
|
11
|
+
* Asserted against the source rather than the built sheet because the build is
|
|
12
|
+
* postcss over this file, and a reader looking for the reset looks here.
|
|
13
|
+
*/
|
|
14
|
+
// `__dirname` rather than `import.meta.url`, which vitest's transform does not
|
|
15
|
+
// leave intact here — the same trap `icons.spec.ts` records.
|
|
16
|
+
//
|
|
17
|
+
// Line endings normalised, and the anchor below is a single line: matching a
|
|
18
|
+
// selector list *across* its own line break reads the file as the checkout wrote
|
|
19
|
+
// it, which on Windows is CRLF. This found nothing there and passed everywhere
|
|
20
|
+
// else, which is the worst way for a check to be wrong.
|
|
21
|
+
const source = readFileSync(resolve(__dirname, '../app/style/main.pcss'), 'utf8').replace(/\r\n/g, '\n')
|
|
22
|
+
|
|
23
|
+
/** The declarations of the first rule whose selector list contains `anchor`. */
|
|
24
|
+
function rule(anchor: string) {
|
|
25
|
+
const start = source.indexOf(anchor)
|
|
26
|
+
if (start === -1) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
const open = source.indexOf('{', start)
|
|
30
|
+
return source.slice(open + 1, source.indexOf('}', open))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The last line of the button reset's selector list. */
|
|
34
|
+
const BUTTON_RESET = '[type=\'submit\']'
|
|
35
|
+
|
|
36
|
+
describe('the manual Preflight replacement', () => {
|
|
37
|
+
it('clears the UA background off a button', () => {
|
|
38
|
+
expect(rule(BUTTON_RESET)).toContain('background-color: transparent')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('clears the UA background image too', () => {
|
|
42
|
+
// Preflight resets both; a gradient theme would otherwise come back.
|
|
43
|
+
expect(rule(BUTTON_RESET)).toContain('background-image: none')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('stays inside `@layer base`, where a utility can still win', () => {
|
|
47
|
+
// An unlayered rule beats every layered one, so `bg-*` on a button would stop
|
|
48
|
+
// working — the trap the file's own comment records.
|
|
49
|
+
const layers = [...source.matchAll(/@layer base \{/g)].map(match => match.index)
|
|
50
|
+
const button = source.indexOf('background-color: transparent')
|
|
51
|
+
expect(layers.some(start => start < button)).toBe(true)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
-
import {
|
|
2
|
+
import { markRaw, ref } from 'vue'
|
|
3
|
+
import { toPresetState, toRawDeep } from '../app/util/state'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* What the state-presets panel does to a variant's state before storing it.
|
|
@@ -64,3 +65,56 @@ describe('a variant\'s state as a preset', () => {
|
|
|
64
65
|
expect(preset.recursiveParameter.name).toBe('hello')
|
|
65
66
|
})
|
|
66
67
|
})
|
|
68
|
+
|
|
69
|
+
describe('a binding Vue has marked raw', () => {
|
|
70
|
+
it('is dropped from what leaves the realm, rather than passed through', () => {
|
|
71
|
+
const graph = markRaw({ nodes: { deep: { deeper: 1 } } })
|
|
72
|
+
|
|
73
|
+
// `clean` means this copy is posted or persisted. `__v_skip` is
|
|
74
|
+
// non-enumerable and does not survive `structuredClone`, so passing the
|
|
75
|
+
// value would hand the far side an unmarked graph to deep-watch — which is
|
|
76
|
+
// the hanging story #957 describes. The key does not go.
|
|
77
|
+
expect(toRawDeep({ text: 'a', graph }, true)).toEqual({ text: 'a' })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('is carried by reference when the copy stays in this realm', () => {
|
|
81
|
+
const graph = markRaw({ nodes: { deep: 1 } })
|
|
82
|
+
const copied = toRawDeep({ text: 'a', graph })
|
|
83
|
+
|
|
84
|
+
// Same identity, not a rebuilt one: that is what lets the compare settle it
|
|
85
|
+
// by `Object.is` instead of walking whatever is behind it.
|
|
86
|
+
expect(copied.graph).toBe(graph)
|
|
87
|
+
expect(copied.text).toBe('a')
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('is unwrapped from a ref first, which is the shape a template ref has', () => {
|
|
91
|
+
const graph = ref(markRaw({ nodes: { deep: 1 } }))
|
|
92
|
+
|
|
93
|
+
expect(toRawDeep({ graph }, true)).toEqual({})
|
|
94
|
+
expect(toRawDeep({ graph }).graph).toBe(graph.value)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('leaves an array the way a function does', () => {
|
|
98
|
+
// Filtered, indices and all. Stated because it is a real consequence and
|
|
99
|
+
// not an obviously right one — it is the rule functions already follow.
|
|
100
|
+
expect(toRawDeep({ list: [1, markRaw({ a: 1 }), 2] }, true)).toEqual({ list: [1, 2] })
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('does not take the whole state with it when the state itself is marked', () => {
|
|
104
|
+
expect(toRawDeep(markRaw({ text: 'a' }), true)).toEqual({})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('takes only itself when it sits inside an ordinary object', () => {
|
|
108
|
+
const state = { config: { zoom: 3, map: markRaw({ tiles: { deep: 1 } }) } }
|
|
109
|
+
|
|
110
|
+
// The far side keeps whatever it had under `config.map`, because a key that
|
|
111
|
+
// is not sent is a key `applyState` cannot overwrite — it merges one level.
|
|
112
|
+
expect(toRawDeep(state, true)).toEqual({ config: { zoom: 3 } })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('never reaches a preset', () => {
|
|
116
|
+
const graph = markRaw({ nodes: { deep: 1 } })
|
|
117
|
+
|
|
118
|
+
expect(toPresetState({ count: 1, graph })).toEqual({ count: 1 })
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isEquivalent } from '@poveste/shared'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { markRaw, reactive } from 'vue'
|
|
4
|
+
import { toRawDeep } from '../app/util/state'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What the walker does to a value that is not a plain object.
|
|
8
|
+
*
|
|
9
|
+
* `postMessage` is structured clone, so structured clone is the boundary that
|
|
10
|
+
* actually exists — and it carries `Date`, `Map`, `Set`, `RegExp`, `Error`,
|
|
11
|
+
* typed arrays and `BigInt` intact. The walker flattened all of them to `{}`
|
|
12
|
+
* before the transport was ever reached, so the question is not which types to
|
|
13
|
+
* support but which ones we were destroying on the way (#977).
|
|
14
|
+
*/
|
|
15
|
+
class Point {
|
|
16
|
+
constructor(public x: number, public y: number) {}
|
|
17
|
+
get len() { return Math.hypot(this.x, this.y) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('a value the transport can carry', () => {
|
|
21
|
+
it('keeps a Date, rather than emptying it', () => {
|
|
22
|
+
const out = toRawDeep({ at: new Date(1790071200000) }, true)
|
|
23
|
+
|
|
24
|
+
expect(out.at).toBeInstanceOf(Date)
|
|
25
|
+
expect(out.at.getTime()).toBe(1790071200000)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps a Map and a Set with their contents', () => {
|
|
29
|
+
const out = toRawDeep({ m: new Map([['a', 1]]), s: new Set([1, 2, 3]) }, true)
|
|
30
|
+
|
|
31
|
+
expect(out.m).toBeInstanceOf(Map)
|
|
32
|
+
expect(out.m.get('a')).toBe(1)
|
|
33
|
+
expect(out.s).toBeInstanceOf(Set)
|
|
34
|
+
expect(out.s.size).toBe(3)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('keeps a RegExp and an Error', () => {
|
|
38
|
+
const out = toRawDeep({ re: /ab+c/gi, err: new Error('boom') }, true)
|
|
39
|
+
|
|
40
|
+
expect(out.re).toBeInstanceOf(RegExp)
|
|
41
|
+
expect(out.re.source).toBe('ab+c')
|
|
42
|
+
expect(out.err).toBeInstanceOf(Error)
|
|
43
|
+
expect(out.err.message).toBe('boom')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('keeps a typed array as one, rather than as an index map', () => {
|
|
47
|
+
const out = toRawDeep({ bytes: new Uint8Array([1, 2, 3]) }, true)
|
|
48
|
+
|
|
49
|
+
expect(out.bytes).toBeInstanceOf(Uint8Array)
|
|
50
|
+
expect([...out.bytes]).toEqual([1, 2, 3])
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('detaches it, so a later mutation does not reach the copy', () => {
|
|
54
|
+
const at = new Date(1790071200000)
|
|
55
|
+
const out = toRawDeep({ at }, true)
|
|
56
|
+
|
|
57
|
+
at.setTime(0)
|
|
58
|
+
|
|
59
|
+
expect(out.at.getTime()).toBe(1790071200000)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('a value the transport refuses', () => {
|
|
64
|
+
it('drops a URL rather than flattening it, the way a function is dropped', () => {
|
|
65
|
+
// Structured clone throws `DataCloneError` on a `URL`. Passing it through
|
|
66
|
+
// would turn a silent flattening into a thrown exception mid-sync, which is
|
|
67
|
+
// worse than what it replaces.
|
|
68
|
+
const out = toRawDeep({ href: new URL('https://poveste.dev/a'), kept: 1 }, true)
|
|
69
|
+
|
|
70
|
+
expect(Object.hasOwn(out, 'href')).toBe(false)
|
|
71
|
+
expect(out.kept).toBe(1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('leaves what it did send intact', () => {
|
|
75
|
+
const out = toRawDeep({ before: 1, href: new URL('https://poveste.dev/a'), after: 2 }, true)
|
|
76
|
+
|
|
77
|
+
expect(out).toEqual({ before: 1, after: 2 })
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
describe('a class instance', () => {
|
|
82
|
+
// Structured clone carries own fields and drops the prototype, so "faithful"
|
|
83
|
+
// has no meaning here. Plain is the answer, and it is now a stated one.
|
|
84
|
+
it('arrives as plain data, with its fields but not its accessors', () => {
|
|
85
|
+
const out = toRawDeep({ p: new Point(3, 4) }, true)
|
|
86
|
+
|
|
87
|
+
expect(out.p).toEqual({ x: 3, y: 4 })
|
|
88
|
+
expect(out.p.len).toBeUndefined()
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('the two halves of the sync agree', () => {
|
|
93
|
+
// `isEquivalent` refuses to compare anything that is not a plain object,
|
|
94
|
+
// because "two distinct `Date`s would otherwise compare equal on an empty key
|
|
95
|
+
// list". That defence could not fire: it is handed the walker's output, and
|
|
96
|
+
// by then the `Date` was already `{}` (#977).
|
|
97
|
+
it('so two different dates are not equivalent after the walk', () => {
|
|
98
|
+
const a = toRawDeep({ at: new Date(1790071200000) }, true)
|
|
99
|
+
const b = toRawDeep({ at: new Date(0) }, true)
|
|
100
|
+
|
|
101
|
+
expect(isEquivalent(a, b)).toBe(false)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('and the in-realm walk carries one by reference, which `Object.is` settles', () => {
|
|
105
|
+
const at = new Date(1790071200000)
|
|
106
|
+
|
|
107
|
+
expect(toRawDeep({ at }, false).at).toBe(at)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe('a value that is reactive, which state always is', () => {
|
|
112
|
+
// The one that would have shipped a second silent failure. Vue proxies `Map`
|
|
113
|
+
// and `Set` but not `Date`, `RegExp`, `Error` or typed arrays, and
|
|
114
|
+
// `structuredClone` throws `DataCloneError` on the proxy while the raw
|
|
115
|
+
// object clones fine. Without `toRaw` a `Map` in reactive state is refused
|
|
116
|
+
// here and dropped — a different silence, not a fix for the first.
|
|
117
|
+
it('clones a reactive Map rather than refusing it', () => {
|
|
118
|
+
const state = reactive({ m: new Map([['a', 1]]) })
|
|
119
|
+
|
|
120
|
+
const out = toRawDeep(state, true)
|
|
121
|
+
|
|
122
|
+
expect(out.m).toBeInstanceOf(Map)
|
|
123
|
+
expect(out.m.get('a')).toBe(1)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('clones a reactive Set the same way', () => {
|
|
127
|
+
const out = toRawDeep(reactive({ s: new Set([1, 2]) }), true)
|
|
128
|
+
|
|
129
|
+
expect(out.s).toBeInstanceOf(Set)
|
|
130
|
+
expect(out.s.size).toBe(2)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('and a reactive Date, which Vue does not proxy', () => {
|
|
134
|
+
const out = toRawDeep(reactive({ at: new Date(1790071200000) }), true)
|
|
135
|
+
|
|
136
|
+
expect(out.at.getTime()).toBe(1790071200000)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('this walk\'s own rules reach inside a Map and a Set', () => {
|
|
141
|
+
// An array already comes out shorter for a function or a marked value. A
|
|
142
|
+
// container is the same problem, and answering it by refusing the whole
|
|
143
|
+
// `Map` would lose the keys beside it.
|
|
144
|
+
it('drops a function from a Map, and keeps the rest', () => {
|
|
145
|
+
const out = toRawDeep({ m: new Map<string, any>([['fn', () => {}], ['kept', 1]]) }, true)
|
|
146
|
+
|
|
147
|
+
expect(out.m.has('fn')).toBe(false)
|
|
148
|
+
expect(out.m.get('kept')).toBe(1)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('drops a marked value from a Set, and keeps the rest', () => {
|
|
152
|
+
const out = toRawDeep({ s: new Set([markRaw({ heavy: true }), 'kept']) }, true)
|
|
153
|
+
|
|
154
|
+
expect([...out.s]).toEqual(['kept'])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('survives a cycle that runs through a Map', () => {
|
|
158
|
+
const root: Record<string, any> = { name: 'root' }
|
|
159
|
+
root.m = new Map([['back', root]])
|
|
160
|
+
|
|
161
|
+
const out = toRawDeep(root, true)
|
|
162
|
+
|
|
163
|
+
expect(out.m.get('back')).toBe(out)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('identity inside one walk', () => {
|
|
168
|
+
it('gives one Date appearing twice the same copy, as the transport would', () => {
|
|
169
|
+
const at = new Date(1790071200000)
|
|
170
|
+
|
|
171
|
+
const out = toRawDeep({ a: at, b: at }, true)
|
|
172
|
+
|
|
173
|
+
expect(out.a).toBe(out.b)
|
|
174
|
+
})
|
|
175
|
+
})
|
package/src/app/App.vue
CHANGED
|
@@ -186,41 +186,50 @@ const storyListVisible = computed(() => !!layoutStore.settings.storyListVisible)
|
|
|
186
186
|
it. The settings toggles were #596; `isMobile` is a live media query,
|
|
187
187
|
so this one fired on a window resize (#600).
|
|
188
188
|
-->
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
189
|
+
<!-- `<main>` wraps the split rather than sitting in its last pane. The
|
|
190
|
+
divider is perceivable now, and it is a sibling of the slot, so it
|
|
191
|
+
belongs to no landmark unless one contains the whole split. Inside
|
|
192
|
+
`<main>` the sidebar's `<header>` is a generic header rather than a
|
|
193
|
+
banner, which is what `landmark-banner-is-top-level` objects to
|
|
194
|
+
when a landmark is put around that pane instead (#995). -->
|
|
195
|
+
<main class="flex-1 min-h-0 flex flex-col">
|
|
196
|
+
<BaseSplitPane
|
|
197
|
+
save-id="main-horiz"
|
|
198
|
+
label="Resize the story list"
|
|
199
|
+
:min="5"
|
|
200
|
+
:max="50"
|
|
201
|
+
:default-split="15"
|
|
202
|
+
:show-first="!isMobile && storyListVisible"
|
|
203
|
+
class="flex-1 min-h-0"
|
|
204
|
+
>
|
|
205
|
+
<template #first>
|
|
206
|
+
<div class="flex flex-col h-full bg-gray-100 dark:bg-gray-750 __poveste-pane-shadow-from-right">
|
|
207
|
+
<AppHeader class="flex-none" />
|
|
208
|
+
<nav
|
|
209
|
+
aria-label="Stories"
|
|
210
|
+
class="flex-1 flex flex-col min-h-0"
|
|
211
|
+
>
|
|
212
|
+
<StoryList
|
|
213
|
+
:tree="tree"
|
|
214
|
+
:stories="stories"
|
|
215
|
+
class="flex-1"
|
|
216
|
+
/>
|
|
217
|
+
</nav>
|
|
218
|
+
</div>
|
|
219
|
+
</template>
|
|
220
|
+
|
|
221
|
+
<template #last>
|
|
222
|
+
<div class="flex flex-col h-full">
|
|
223
|
+
<TopBar
|
|
224
|
+
v-if="!isMobile"
|
|
225
|
+
@layout="isLayoutOpen = true"
|
|
226
|
+
@search="isSearchOpen = true"
|
|
208
227
|
/>
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
<main class="flex flex-col h-full">
|
|
215
|
-
<TopBar
|
|
216
|
-
v-if="!isMobile"
|
|
217
|
-
@layout="isLayoutOpen = true"
|
|
218
|
-
@search="isSearchOpen = true"
|
|
219
|
-
/>
|
|
220
|
-
<RouterView class="flex-1 min-h-0" />
|
|
221
|
-
</main>
|
|
222
|
-
</template>
|
|
223
|
-
</BaseSplitPane>
|
|
228
|
+
<RouterView class="flex-1 min-h-0" />
|
|
229
|
+
</div>
|
|
230
|
+
</template>
|
|
231
|
+
</BaseSplitPane>
|
|
232
|
+
</main>
|
|
224
233
|
</div>
|
|
225
234
|
|
|
226
235
|
<LayoutModal
|
|
@@ -4,7 +4,7 @@ import { computed } from 'vue'
|
|
|
4
4
|
import { povesteConfig } from '../../util/config'
|
|
5
5
|
import { isDark, toggleDark } from '../../util/dark'
|
|
6
6
|
import { isMobile } from '../../util/responsive'
|
|
7
|
-
import
|
|
7
|
+
import ShortcutTooltip from './ShortcutTooltip.vue'
|
|
8
8
|
import TopBarChip from './TopBarChip.vue'
|
|
9
9
|
import TopBarChipItem from './TopBarChipItem.vue'
|
|
10
10
|
|
|
@@ -20,41 +20,53 @@ const themeIcon = computed(() => {
|
|
|
20
20
|
|
|
21
21
|
<template>
|
|
22
22
|
<TopBarChip>
|
|
23
|
-
<
|
|
23
|
+
<ShortcutTooltip
|
|
24
24
|
v-if="!isMobile"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
data-testid="layout-btn"
|
|
28
|
-
@click="$emit('layout')"
|
|
25
|
+
description="Layout"
|
|
26
|
+
:shortcut="({ isMac }) => isMac ? 'meta+shift+l' : 'ctrl+shift+l'"
|
|
29
27
|
>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
<TopBarChipItem
|
|
29
|
+
aria-label="Open layout customization"
|
|
30
|
+
data-testid="layout-btn"
|
|
31
|
+
@click="$emit('layout')"
|
|
32
|
+
>
|
|
33
|
+
<Icon
|
|
34
|
+
icon="carbon:panel-expansion"
|
|
35
|
+
class="w-4 h-4"
|
|
36
|
+
/>
|
|
37
|
+
</TopBarChipItem>
|
|
38
|
+
</ShortcutTooltip>
|
|
35
39
|
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
data-testid="search-btn"
|
|
40
|
-
@click="$emit('search')"
|
|
40
|
+
<ShortcutTooltip
|
|
41
|
+
description="Search"
|
|
42
|
+
:shortcut="({ isMac }) => isMac ? 'meta+k' : 'ctrl+k'"
|
|
41
43
|
>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
<TopBarChipItem
|
|
45
|
+
aria-label="Search stories"
|
|
46
|
+
data-testid="search-btn"
|
|
47
|
+
@click="$emit('search')"
|
|
48
|
+
>
|
|
49
|
+
<Icon
|
|
50
|
+
icon="carbon:search"
|
|
51
|
+
class="w-4 h-4"
|
|
52
|
+
/>
|
|
53
|
+
</TopBarChipItem>
|
|
54
|
+
</ShortcutTooltip>
|
|
47
55
|
|
|
48
|
-
<
|
|
56
|
+
<ShortcutTooltip
|
|
49
57
|
v-if="!povesteConfig.theme.hideColorSchemeSwitch"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@click="toggleDark()"
|
|
58
|
+
description="Toggle dark mode"
|
|
59
|
+
:shortcut="({ isMac }) => isMac ? 'meta+shift+d' : 'ctrl+shift+d'"
|
|
53
60
|
>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
<TopBarChipItem
|
|
62
|
+
aria-label="Toggle dark mode"
|
|
63
|
+
@click="toggleDark()"
|
|
64
|
+
>
|
|
65
|
+
<Icon
|
|
66
|
+
:icon="themeIcon"
|
|
67
|
+
class="w-4 h-4"
|
|
68
|
+
/>
|
|
69
|
+
</TopBarChipItem>
|
|
70
|
+
</ShortcutTooltip>
|
|
59
71
|
</TopBarChip>
|
|
60
72
|
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { HstTooltip } from '@poveste/controls'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { isMac } from '../../util/env'
|
|
5
|
+
import { formatKey } from '../../util/keyboard'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
description: string
|
|
9
|
+
/** This action's shortcut, which differs per platform — `meta+shift+l`. */
|
|
10
|
+
shortcut: (options: { isMac: boolean }) => string
|
|
11
|
+
}>()
|
|
12
|
+
|
|
13
|
+
const keys = computed(() => props.shortcut({ isMac }).split('+').map(key => formatKey(key.trim())).join(' '))
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<!-- Rendered rather than built as an HTML string and handed to `v-html`, which
|
|
18
|
+
is what the tooltip directive needed and what `util/tooltip.ts` was. -->
|
|
19
|
+
<HstTooltip>
|
|
20
|
+
<template #content>
|
|
21
|
+
<div>{{ description }}</div>
|
|
22
|
+
<div class="flex items-center gap-1 mt-2 text-sm">
|
|
23
|
+
<span class="border border-gray-600 px-1 rounded-sm text-gray-400">{{ keys }}</span>
|
|
24
|
+
</div>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<slot />
|
|
28
|
+
</HstTooltip>
|
|
29
|
+
</template>
|