@furystack/shades 14.0.0 → 15.0.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/CHANGELOG.md +30 -0
- package/esm/compile-route.d.ts +5 -0
- package/esm/compile-route.d.ts.map +1 -1
- package/esm/compile-route.js +5 -0
- package/esm/compile-route.js.map +1 -1
- package/esm/component-factory.spec.d.ts.map +1 -1
- package/esm/component-factory.spec.js +2 -2
- package/esm/component-factory.spec.js.map +1 -1
- package/esm/components/nested-hooks.js +2 -2
- package/esm/components/nested-hooks.js.map +1 -1
- package/esm/components/nested-hooks.spec.js +14 -14
- package/esm/components/nested-hooks.spec.js.map +1 -1
- package/esm/components/nested-navigate.js +1 -1
- package/esm/components/nested-navigate.js.map +1 -1
- package/esm/components/nested-navigate.spec.js +16 -16
- package/esm/components/nested-navigate.spec.js.map +1 -1
- package/esm/components/nested-replace.js +1 -1
- package/esm/components/nested-replace.js.map +1 -1
- package/esm/components/nested-replace.spec.js +6 -6
- package/esm/components/nested-replace.spec.js.map +1 -1
- package/esm/components/nested-route-link.js +1 -1
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +8 -8
- package/esm/components/nested-route-link.spec.js.map +1 -1
- package/esm/components/nested-router.js +3 -3
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/nested-router.spec.js +24 -24
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/css-generator.d.ts +9 -26
- package/esm/css-generator.d.ts.map +1 -1
- package/esm/css-generator.js +9 -26
- package/esm/css-generator.js.map +1 -1
- package/esm/css-generator.spec.js.map +1 -1
- package/esm/models/css-object.d.ts +3 -5
- package/esm/models/css-object.d.ts.map +1 -1
- package/esm/models/render-options.d.ts +16 -20
- package/esm/models/render-options.d.ts.map +1 -1
- package/esm/models/shade-component.d.ts +6 -4
- package/esm/models/shade-component.d.ts.map +1 -1
- package/esm/models/shade-component.js +3 -3
- package/esm/models/shade-component.js.map +1 -1
- package/esm/services/location-service.d.ts +39 -42
- package/esm/services/location-service.d.ts.map +1 -1
- package/esm/services/location-service.js +115 -134
- package/esm/services/location-service.js.map +1 -1
- package/esm/services/location-service.spec.js +32 -30
- package/esm/services/location-service.spec.js.map +1 -1
- package/esm/services/route-match-service.d.ts +3 -2
- package/esm/services/route-match-service.d.ts.map +1 -1
- package/esm/services/route-match-service.js +13 -22
- package/esm/services/route-match-service.js.map +1 -1
- package/esm/services/route-match-service.spec.js +40 -51
- package/esm/services/route-match-service.spec.js.map +1 -1
- package/esm/services/route-meta-utils.js.map +1 -1
- package/esm/services/route-meta-utils.spec.js +3 -3
- package/esm/services/route-meta-utils.spec.js.map +1 -1
- package/esm/services/screen-service.d.ts +13 -72
- package/esm/services/screen-service.d.ts.map +1 -1
- package/esm/services/screen-service.js +45 -137
- package/esm/services/screen-service.js.map +1 -1
- package/esm/services/screen-service.spec.js +19 -18
- package/esm/services/screen-service.spec.js.map +1 -1
- package/esm/services/spatial-navigation-service.d.ts +17 -39
- package/esm/services/spatial-navigation-service.d.ts.map +1 -1
- package/esm/services/spatial-navigation-service.js +280 -313
- package/esm/services/spatial-navigation-service.js.map +1 -1
- package/esm/services/spatial-navigation-service.spec.js +163 -158
- package/esm/services/spatial-navigation-service.spec.js.map +1 -1
- package/esm/shade-component.d.ts +14 -17
- package/esm/shade-component.d.ts.map +1 -1
- package/esm/shade-component.js +14 -17
- package/esm/shade-component.js.map +1 -1
- package/esm/shade.d.ts +18 -15
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +22 -32
- package/esm/shade.js.map +1 -1
- package/esm/style-manager.d.ts +8 -22
- package/esm/style-manager.d.ts.map +1 -1
- package/esm/style-manager.js +8 -22
- package/esm/style-manager.js.map +1 -1
- package/esm/styled-shade.d.ts +4 -7
- package/esm/styled-shade.d.ts.map +1 -1
- package/esm/styled-shade.js +4 -4
- package/esm/styled-shade.js.map +1 -1
- package/esm/view-transition.d.ts +10 -22
- package/esm/view-transition.d.ts.map +1 -1
- package/esm/view-transition.js +10 -22
- package/esm/view-transition.js.map +1 -1
- package/esm/vnode.js.map +1 -1
- package/esm/vnode.spec.js.map +1 -1
- package/package.json +9 -8
- package/src/compile-route.ts +5 -0
- package/src/component-factory.spec.tsx +3 -2
- package/src/components/nested-hooks.spec.ts +14 -14
- package/src/components/nested-hooks.ts +2 -2
- package/src/components/nested-navigate.spec.ts +17 -16
- package/src/components/nested-navigate.ts +1 -1
- package/src/components/nested-replace.spec.ts +7 -6
- package/src/components/nested-replace.ts +1 -1
- package/src/components/nested-route-link.spec.tsx +8 -8
- package/src/components/nested-route-link.tsx +1 -1
- package/src/components/nested-router.spec.tsx +24 -24
- package/src/components/nested-router.tsx +5 -5
- package/src/css-generator.spec.ts +1 -1
- package/src/css-generator.ts +9 -26
- package/src/models/css-object.ts +3 -5
- package/src/models/render-options.ts +17 -20
- package/src/models/shade-component.ts +6 -4
- package/src/services/location-service.spec.ts +35 -31
- package/src/services/location-service.tsx +156 -122
- package/src/services/route-match-service.spec.ts +41 -32
- package/src/services/route-match-service.ts +17 -8
- package/src/services/route-meta-utils.spec.ts +3 -3
- package/src/services/route-meta-utils.ts +1 -1
- package/src/services/screen-service.spec.ts +21 -20
- package/src/services/screen-service.ts +65 -110
- package/src/services/spatial-navigation-service.spec.ts +165 -160
- package/src/services/spatial-navigation-service.ts +299 -287
- package/src/shade-component.ts +14 -17
- package/src/shade.ts +31 -41
- package/src/style-manager.ts +9 -22
- package/src/styled-shade.ts +5 -8
- package/src/view-transition.ts +10 -22
- package/src/vnode.spec.ts +3 -3
- package/src/vnode.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInjector } from '@furystack/inject'
|
|
2
2
|
import { serializeValue } from '@furystack/rest'
|
|
3
3
|
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -438,7 +438,7 @@ describe('NestedRouter lifecycle hooks', () => {
|
|
|
438
438
|
callOrder.push('leave-other')
|
|
439
439
|
})
|
|
440
440
|
|
|
441
|
-
await usingAsync(
|
|
441
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
442
442
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
443
443
|
|
|
444
444
|
initializeShadeRoot({
|
|
@@ -556,7 +556,7 @@ describe('NestedRouter query / hash re-render', () => {
|
|
|
556
556
|
const onLeave = vi.fn(async () => {})
|
|
557
557
|
const componentFn = vi.fn(({ hash }: { hash: string | undefined }) => <div id="content">hash={hash ?? 'none'}</div>)
|
|
558
558
|
|
|
559
|
-
await usingAsync(
|
|
559
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
560
560
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
561
561
|
|
|
562
562
|
initializeShadeRoot({
|
|
@@ -581,7 +581,7 @@ describe('NestedRouter query / hash re-render', () => {
|
|
|
581
581
|
expect(onVisit).toBeCalledTimes(1)
|
|
582
582
|
expect(onLeave).not.toBeCalled()
|
|
583
583
|
|
|
584
|
-
const locationService = injector.
|
|
584
|
+
const locationService = injector.get(LocationService)
|
|
585
585
|
locationService.navigate('/tabs#a')
|
|
586
586
|
await flushUpdates()
|
|
587
587
|
await flushUpdates()
|
|
@@ -609,7 +609,7 @@ describe('NestedRouter query / hash re-render', () => {
|
|
|
609
609
|
<div id="content">page={query?.page ?? 'none'}</div>
|
|
610
610
|
))
|
|
611
611
|
|
|
612
|
-
await usingAsync(
|
|
612
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
613
613
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
614
614
|
|
|
615
615
|
initializeShadeRoot({
|
|
@@ -633,7 +633,7 @@ describe('NestedRouter query / hash re-render', () => {
|
|
|
633
633
|
expect(document.getElementById('content')?.innerHTML).toBe('page=none')
|
|
634
634
|
expect(onVisit).toBeCalledTimes(1)
|
|
635
635
|
|
|
636
|
-
const locationService = injector.
|
|
636
|
+
const locationService = injector.get(LocationService)
|
|
637
637
|
locationService.navigate(`/list?page=${serializeValue(2)}`)
|
|
638
638
|
await flushUpdates()
|
|
639
639
|
await flushUpdates()
|
|
@@ -662,7 +662,7 @@ describe('NestedRouter query / hash re-render', () => {
|
|
|
662
662
|
</div>
|
|
663
663
|
))
|
|
664
664
|
|
|
665
|
-
await usingAsync(
|
|
665
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
666
666
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
667
667
|
|
|
668
668
|
initializeShadeRoot({
|
|
@@ -686,7 +686,7 @@ describe('NestedRouter query / hash re-render', () => {
|
|
|
686
686
|
expect(document.getElementById('content')?.innerHTML).toBe('a')
|
|
687
687
|
bComponent.mockClear()
|
|
688
688
|
|
|
689
|
-
injector.
|
|
689
|
+
injector.get(LocationService).navigate(`/b?page=${serializeValue(7)}#x`)
|
|
690
690
|
await flushUpdates()
|
|
691
691
|
await flushUpdates()
|
|
692
692
|
|
|
@@ -731,7 +731,7 @@ describe('NestedRouter latest-wins on rapid navigation', () => {
|
|
|
731
731
|
callOrder.push('leave-c')
|
|
732
732
|
})
|
|
733
733
|
|
|
734
|
-
await usingAsync(
|
|
734
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
735
735
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
736
736
|
|
|
737
737
|
initializeShadeRoot({
|
|
@@ -813,7 +813,7 @@ describe('NestedRouter lifecycle element scope', () => {
|
|
|
813
813
|
const visitElements: Array<{ route: string; element: JSX.Element }> = []
|
|
814
814
|
const leaveElements: Array<{ route: string; element: JSX.Element }> = []
|
|
815
815
|
|
|
816
|
-
await usingAsync(
|
|
816
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
817
817
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
818
818
|
|
|
819
819
|
initializeShadeRoot({
|
|
@@ -906,7 +906,7 @@ describe('NestedRouter flat routes', () => {
|
|
|
906
906
|
it('should render and navigate between flat (non-nested) Record routes', async () => {
|
|
907
907
|
history.pushState(null, '', '/')
|
|
908
908
|
|
|
909
|
-
await usingAsync(
|
|
909
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
910
910
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
911
911
|
|
|
912
912
|
initializeShadeRoot({
|
|
@@ -967,7 +967,7 @@ describe('NestedRouter outlet composition', () => {
|
|
|
967
967
|
it('should compose parent layout wrapping child content via outlet', async () => {
|
|
968
968
|
history.pushState(null, '', '/dashboard/settings')
|
|
969
969
|
|
|
970
|
-
await usingAsync(
|
|
970
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
971
971
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
972
972
|
|
|
973
973
|
initializeShadeRoot({
|
|
@@ -1012,7 +1012,7 @@ describe('NestedRouter outlet composition', () => {
|
|
|
1012
1012
|
it('should render parent with fallback when navigating to parent URL without a child match', async () => {
|
|
1013
1013
|
history.pushState(null, '', '/dashboard')
|
|
1014
1014
|
|
|
1015
|
-
await usingAsync(
|
|
1015
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1016
1016
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1017
1017
|
|
|
1018
1018
|
initializeShadeRoot({
|
|
@@ -1065,7 +1065,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
1065
1065
|
callOrder.push('leave-user')
|
|
1066
1066
|
})
|
|
1067
1067
|
|
|
1068
|
-
await usingAsync(
|
|
1068
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1069
1069
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1070
1070
|
|
|
1071
1071
|
initializeShadeRoot({
|
|
@@ -1141,7 +1141,7 @@ describe('NestedRouter route param changes', () => {
|
|
|
1141
1141
|
const onVisitDash = vi.fn()
|
|
1142
1142
|
const onLeaveDash = vi.fn()
|
|
1143
1143
|
|
|
1144
|
-
await usingAsync(
|
|
1144
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1145
1145
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1146
1146
|
|
|
1147
1147
|
initializeShadeRoot({
|
|
@@ -1214,9 +1214,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
|
|
|
1214
1214
|
it('should update RouteMatchService with the current match chain on navigation', async () => {
|
|
1215
1215
|
history.pushState(null, '', '/parent/child-a')
|
|
1216
1216
|
|
|
1217
|
-
await usingAsync(
|
|
1217
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1218
1218
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1219
|
-
const routeMatchService = injector.
|
|
1219
|
+
const routeMatchService = injector.get(RouteMatchService)
|
|
1220
1220
|
|
|
1221
1221
|
const parentRoute: NestedRoute = {
|
|
1222
1222
|
meta: { title: 'Parent' },
|
|
@@ -1278,9 +1278,9 @@ describe('NestedRouter + RouteMatchService integration', () => {
|
|
|
1278
1278
|
it('should expose match params through RouteMatchService', async () => {
|
|
1279
1279
|
history.pushState(null, '', '/users/42')
|
|
1280
1280
|
|
|
1281
|
-
await usingAsync(
|
|
1281
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1282
1282
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1283
|
-
const routeMatchService = injector.
|
|
1283
|
+
const routeMatchService = injector.get(RouteMatchService)
|
|
1284
1284
|
|
|
1285
1285
|
initializeShadeRoot({
|
|
1286
1286
|
injector,
|
|
@@ -1378,7 +1378,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1378
1378
|
it('should call startViewTransition when viewTransition is enabled', async () => {
|
|
1379
1379
|
history.pushState(null, '', '/')
|
|
1380
1380
|
|
|
1381
|
-
await usingAsync(
|
|
1381
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1382
1382
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1383
1383
|
|
|
1384
1384
|
initializeShadeRoot({
|
|
@@ -1414,7 +1414,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1414
1414
|
it('should not call startViewTransition when viewTransition is not set', async () => {
|
|
1415
1415
|
history.pushState(null, '', '/')
|
|
1416
1416
|
|
|
1417
|
-
await usingAsync(
|
|
1417
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1418
1418
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1419
1419
|
|
|
1420
1420
|
initializeShadeRoot({
|
|
@@ -1449,7 +1449,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1449
1449
|
it('should pass types to startViewTransition when configured', async () => {
|
|
1450
1450
|
history.pushState(null, '', '/')
|
|
1451
1451
|
|
|
1452
|
-
await usingAsync(
|
|
1452
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1453
1453
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1454
1454
|
|
|
1455
1455
|
initializeShadeRoot({
|
|
@@ -1484,7 +1484,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1484
1484
|
it('should respect per-route viewTransition: false override', async () => {
|
|
1485
1485
|
history.pushState(null, '', '/')
|
|
1486
1486
|
|
|
1487
|
-
await usingAsync(
|
|
1487
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1488
1488
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1489
1489
|
|
|
1490
1490
|
initializeShadeRoot({
|
|
@@ -1525,7 +1525,7 @@ describe('NestedRouter view transitions', () => {
|
|
|
1525
1525
|
|
|
1526
1526
|
history.pushState(null, '', '/')
|
|
1527
1527
|
|
|
1528
|
-
await usingAsync(
|
|
1528
|
+
await usingAsync(createInjector(), async (injector) => {
|
|
1529
1529
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
1530
1530
|
|
|
1531
1531
|
initializeShadeRoot({
|
|
@@ -271,8 +271,8 @@ const isShallowEqual = (a: unknown, b: unknown): boolean => {
|
|
|
271
271
|
}
|
|
272
272
|
if (Array.isArray(b)) return false
|
|
273
273
|
|
|
274
|
-
const aKeys = Object.keys(a
|
|
275
|
-
const bKeys = Object.keys(b
|
|
274
|
+
const aKeys = Object.keys(a)
|
|
275
|
+
const bKeys = Object.keys(b)
|
|
276
276
|
if (aKeys.length !== bKeys.length) return false
|
|
277
277
|
for (const key of aKeys) {
|
|
278
278
|
if (!Object.prototype.hasOwnProperty.call(b, key)) return false
|
|
@@ -386,7 +386,7 @@ export const NestedRouter = Shade<NestedRouterProps>({
|
|
|
386
386
|
chainElements: [],
|
|
387
387
|
})
|
|
388
388
|
|
|
389
|
-
const locationService = injector.
|
|
389
|
+
const locationService = injector.get(LocationService)
|
|
390
390
|
|
|
391
391
|
const updateUrl = async (currentUrl: string) => {
|
|
392
392
|
const [lastState] = useState<NestedRouterState>('routerState', state)
|
|
@@ -426,7 +426,7 @@ export const NestedRouter = Shade<NestedRouterProps>({
|
|
|
426
426
|
|
|
427
427
|
const applyUpdate = () => {
|
|
428
428
|
setState({ matchChain: newChain, jsx: newResult.jsx, chainElements: newResult.chainElements })
|
|
429
|
-
injector.
|
|
429
|
+
injector.get(RouteMatchService).currentMatchChain.setValue(newChain)
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
const vtConfig = resolveViewTransition(options.props.viewTransition, newChain)
|
|
@@ -451,7 +451,7 @@ export const NestedRouter = Shade<NestedRouterProps>({
|
|
|
451
451
|
jsx: options.props.notFound || <div />,
|
|
452
452
|
chainElements: [],
|
|
453
453
|
})
|
|
454
|
-
injector.
|
|
454
|
+
injector.get(RouteMatchService).currentMatchChain.setValue([])
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
await maybeViewTransition(options.props.viewTransition, applyNotFound)
|
|
@@ -71,7 +71,7 @@ describe('css-generator', () => {
|
|
|
71
71
|
color: 'red',
|
|
72
72
|
'&:hover': { color: 'blue' },
|
|
73
73
|
}
|
|
74
|
-
const result = propertiesToCSSString(mixedObject
|
|
74
|
+
const result = propertiesToCSSString(mixedObject)
|
|
75
75
|
expect(result).toBe('color: red')
|
|
76
76
|
})
|
|
77
77
|
|
package/src/css-generator.ts
CHANGED
|
@@ -1,30 +1,20 @@
|
|
|
1
1
|
import type { CSSObject, CSSProperties } from './models/css-object.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Converts a camelCase string to kebab-case
|
|
5
|
-
* @param str - The camelCase string to convert
|
|
6
|
-
* @returns The kebab-case string
|
|
7
4
|
* @example
|
|
8
5
|
* camelToKebab('backgroundColor') // 'background-color'
|
|
9
|
-
* camelToKebab('fontSize') // 'font-size'
|
|
10
6
|
*/
|
|
11
7
|
export const camelToKebab = (str: string): string => {
|
|
12
8
|
return str.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`)
|
|
13
9
|
}
|
|
14
10
|
|
|
15
|
-
/**
|
|
16
|
-
* Checks if a key is a selector key (starts with '&')
|
|
17
|
-
* @param key - The key to check
|
|
18
|
-
* @returns True if the key is a selector key
|
|
19
|
-
*/
|
|
20
11
|
export const isSelectorKey = (key: string): boolean => {
|
|
21
12
|
return key.startsWith('&')
|
|
22
13
|
}
|
|
23
14
|
|
|
24
15
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @returns A CSS declaration string (e.g., "color: red; padding: 10px;")
|
|
16
|
+
* Serializes the non-selector entries of a {@link CSSProperties} object as
|
|
17
|
+
* a CSS declaration body. Selector keys (`&...`) are skipped.
|
|
28
18
|
*/
|
|
29
19
|
export const propertiesToCSSString = (properties: CSSProperties): string => {
|
|
30
20
|
const declarations: string[] = []
|
|
@@ -41,12 +31,7 @@ export const propertiesToCSSString = (properties: CSSProperties): string => {
|
|
|
41
31
|
return declarations.join('; ')
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
/**
|
|
45
|
-
* Generates a CSS rule string from a selector and properties
|
|
46
|
-
* @param selector - The CSS selector
|
|
47
|
-
* @param properties - The CSS properties object
|
|
48
|
-
* @returns A complete CSS rule string (e.g., "selector { color: red; }")
|
|
49
|
-
*/
|
|
34
|
+
/** Builds `selector { decls; }` from the non-selector entries. Empty when no decls. */
|
|
50
35
|
export const generateCSSRule = (selector: string, properties: CSSProperties): string => {
|
|
51
36
|
const cssString = propertiesToCSSString(properties)
|
|
52
37
|
if (!cssString) {
|
|
@@ -56,10 +41,9 @@ export const generateCSSRule = (selector: string, properties: CSSProperties): st
|
|
|
56
41
|
}
|
|
57
42
|
|
|
58
43
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* @returns A complete CSS string with all rules
|
|
44
|
+
* Renders a {@link CSSObject} as CSS rules. Base properties become a single
|
|
45
|
+
* rule against `selector`; each `&...` selector key produces an additional
|
|
46
|
+
* rule with `&` substituted for `selector`.
|
|
63
47
|
*
|
|
64
48
|
* @example
|
|
65
49
|
* ```typescript
|
|
@@ -68,10 +52,9 @@ export const generateCSSRule = (selector: string, properties: CSSProperties): st
|
|
|
68
52
|
* '&:hover': { color: 'blue' },
|
|
69
53
|
* '& .inner': { fontWeight: 'bold' }
|
|
70
54
|
* })
|
|
71
|
-
* //
|
|
72
|
-
* //
|
|
73
|
-
* //
|
|
74
|
-
* // my-component .inner { font-weight: bold; }"
|
|
55
|
+
* // my-component { color: red; }
|
|
56
|
+
* // my-component:hover { color: blue; }
|
|
57
|
+
* // my-component .inner { font-weight: bold; }
|
|
75
58
|
* ```
|
|
76
59
|
*/
|
|
77
60
|
export const generateCSS = (selector: string, cssObject: CSSObject): string => {
|
package/src/models/css-object.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base CSS properties - subset of CSSStyleDeclaration
|
|
3
|
-
*/
|
|
4
1
|
export type CSSProperties = Partial<CSSStyleDeclaration>
|
|
5
2
|
|
|
6
3
|
/**
|
|
7
|
-
* Selector
|
|
8
|
-
*
|
|
4
|
+
* Selector keys for pseudo-classes and nested selectors. The leading `&`
|
|
5
|
+
* is replaced with the host selector at registration time
|
|
6
|
+
* (e.g. `&:hover` → `my-element:hover`).
|
|
9
7
|
*/
|
|
10
8
|
export type SelectorKey = `&${string}`
|
|
11
9
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject'
|
|
2
2
|
import type { ObservableValue, ValueObserverOptions } from '@furystack/utils'
|
|
3
|
+
import type { LocationService } from '../services/location-service.js'
|
|
3
4
|
import type { ChildrenList } from './children-list.js'
|
|
4
5
|
import type { PartialElement } from './partial-element.js'
|
|
5
6
|
|
|
@@ -79,13 +80,11 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
|
|
|
79
80
|
useRef: <T extends Element = HTMLElement>(key: string) => RefObject<T>
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
|
-
* Creates
|
|
83
|
-
*
|
|
84
|
-
* the
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* @param deps Optional dependency array — when deps change, the old resource is disposed and a new one is created
|
|
88
|
-
* @returns The Disposable instance
|
|
83
|
+
* Creates a disposable resource the first time it is requested per `key`,
|
|
84
|
+
* caches it across renders, and disposes it on `disconnectedCallback`.
|
|
85
|
+
* When `deps` is provided, the cached resource is disposed + re-created
|
|
86
|
+
* whenever the serialized deps change (use for `key`-stable resources
|
|
87
|
+
* that depend on dynamic inputs).
|
|
89
88
|
*/
|
|
90
89
|
useDisposable: <T extends Disposable | AsyncDisposable>(key: string, factory: () => T, deps?: readonly unknown[]) => T
|
|
91
90
|
|
|
@@ -121,27 +120,25 @@ export type RenderOptions<TProps, TElementBase extends HTMLElement = HTMLElement
|
|
|
121
120
|
) => [value: T, setValue: (newValue: T) => void]
|
|
122
121
|
|
|
123
122
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
123
|
+
* Local component state. Returns `[value, setValue]`; calling the setter
|
|
124
|
+
* with a new value triggers a re-render. Preferred over manually wiring
|
|
125
|
+
* an `ObservableValue` + `useObservable` (`furystack/prefer-use-state`
|
|
126
|
+
* enforces this).
|
|
128
127
|
*/
|
|
129
128
|
useState: <T>(key: string, initialValue: T) => [value: T, setValue: (newValue: T) => void]
|
|
130
129
|
|
|
131
130
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
131
|
+
* State backed by a URL search-string parameter via {@link LocationService}.
|
|
132
|
+
* Bidirectional: setting the value pushes a history entry; navigating
|
|
133
|
+
* (back/forward, manual edit) updates the value. `initialValue` applies
|
|
134
|
+
* when the key is absent from the current URL.
|
|
136
135
|
*/
|
|
137
136
|
useSearchState: <T>(key: string, initialValue: T) => [value: T, setValue: (newValue: T) => void]
|
|
138
137
|
|
|
139
138
|
/**
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* @param storageArea The storage area to use
|
|
144
|
-
* @returns a tuple with the current value and a setter function
|
|
139
|
+
* State backed by `localStorage` (or a custom `Storage`). Synchronizes
|
|
140
|
+
* across tabs via the `storage` event and a `BroadcastChannel`.
|
|
141
|
+
* `initialValue` applies when the key is absent from `storageArea`.
|
|
145
142
|
*/
|
|
146
143
|
useStoredState: <T>(
|
|
147
144
|
key: string,
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { ChildrenList } from './children-list.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Callable that produces a JSX element from props (and optional children).
|
|
5
|
+
* Both `Shade(...)` factories and plain functional components conform to
|
|
6
|
+
* this shape.
|
|
5
7
|
*/
|
|
6
8
|
export type ShadeComponent<TProps = object> = (arg: TProps, children?: ChildrenList) => JSX.Element
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Discriminates {@link ShadeComponent} from intrinsic-element tag names.
|
|
12
|
+
* Intentionally permissive — any function is treated as a component since
|
|
13
|
+
* VNodes carry no other discriminator.
|
|
12
14
|
*/
|
|
13
15
|
export const isShadeComponent = <T = any>(obj: any): obj is ShadeComponent<T> => {
|
|
14
16
|
return typeof obj === 'function'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInjector } from '@furystack/inject'
|
|
2
2
|
import { deserializeQueryString, serializeToQueryString, serializeValue } from '@furystack/rest'
|
|
3
3
|
import { usingAsync } from '@furystack/utils'
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
@@ -13,16 +13,17 @@ describe('LocationService', () => {
|
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
it('Shuld be constructed', async () => {
|
|
16
|
-
await usingAsync(
|
|
17
|
-
const s = i.
|
|
18
|
-
expect(s).
|
|
16
|
+
await usingAsync(createInjector(), async (i) => {
|
|
17
|
+
const s = i.get(LocationService)
|
|
18
|
+
expect(s).toBeDefined()
|
|
19
|
+
expect(typeof s.navigate).toBe('function')
|
|
19
20
|
})
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
it('Shuld update state on events', async () => {
|
|
23
|
-
await usingAsync(
|
|
24
|
+
await usingAsync(createInjector(), async (i) => {
|
|
24
25
|
const onLocaionChanged = vi.fn()
|
|
25
|
-
const s = i.
|
|
26
|
+
const s = i.get(LocationService)
|
|
26
27
|
s.onLocationPathChanged.subscribe(onLocaionChanged)
|
|
27
28
|
expect(onLocaionChanged).toBeCalledTimes(0)
|
|
28
29
|
history.pushState(null, '', '/loc1')
|
|
@@ -39,9 +40,9 @@ describe('LocationService', () => {
|
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
it('Should update location path when navigate is called', async () => {
|
|
42
|
-
await usingAsync(
|
|
43
|
+
await usingAsync(createInjector(), async (i) => {
|
|
43
44
|
const onLocationChanged = vi.fn()
|
|
44
|
-
const s = i.
|
|
45
|
+
const s = i.get(LocationService)
|
|
45
46
|
s.onLocationPathChanged.subscribe(onLocationChanged)
|
|
46
47
|
s.navigate('/dashboard')
|
|
47
48
|
expect(s.onLocationPathChanged.getValue()).toBe('/dashboard')
|
|
@@ -51,8 +52,8 @@ describe('LocationService', () => {
|
|
|
51
52
|
|
|
52
53
|
describe('replace', () => {
|
|
53
54
|
it('Should update the observable path without pushing a new history entry', async () => {
|
|
54
|
-
await usingAsync(
|
|
55
|
-
const s = i.
|
|
55
|
+
await usingAsync(createInjector(), async (i) => {
|
|
56
|
+
const s = i.get(LocationService)
|
|
56
57
|
const lengthBefore = history.length
|
|
57
58
|
s.replace('/replaced')
|
|
58
59
|
expect(s.onLocationPathChanged.getValue()).toBe('/replaced')
|
|
@@ -61,8 +62,8 @@ describe('LocationService', () => {
|
|
|
61
62
|
})
|
|
62
63
|
|
|
63
64
|
it('Should call history.replaceState rather than pushState', async () => {
|
|
64
|
-
await usingAsync(
|
|
65
|
-
const s = i.
|
|
65
|
+
await usingAsync(createInjector(), async (i) => {
|
|
66
|
+
const s = i.get(LocationService)
|
|
66
67
|
const pushSpy = vi.spyOn(history, 'pushState')
|
|
67
68
|
const replaceSpy = vi.spyOn(history, 'replaceState')
|
|
68
69
|
|
|
@@ -78,8 +79,8 @@ describe('LocationService', () => {
|
|
|
78
79
|
})
|
|
79
80
|
|
|
80
81
|
it('Should notify path subscribers after replace', async () => {
|
|
81
|
-
await usingAsync(
|
|
82
|
-
const s = i.
|
|
82
|
+
await usingAsync(createInjector(), async (i) => {
|
|
83
|
+
const s = i.get(LocationService)
|
|
83
84
|
const onLocationChanged = vi.fn()
|
|
84
85
|
s.onLocationPathChanged.subscribe(onLocationChanged)
|
|
85
86
|
s.replace('/notify')
|
|
@@ -90,8 +91,8 @@ describe('LocationService', () => {
|
|
|
90
91
|
|
|
91
92
|
describe('useSearchParam', () => {
|
|
92
93
|
it('Should create observables lazily', async () => {
|
|
93
|
-
await usingAsync(
|
|
94
|
-
const service = i.
|
|
94
|
+
await usingAsync(createInjector(), async (i) => {
|
|
95
|
+
const service = i.get(LocationService)
|
|
95
96
|
const observables = service.searchParamObservables
|
|
96
97
|
|
|
97
98
|
const testSearchParam = service.useSearchParam('test', null)
|
|
@@ -110,16 +111,16 @@ describe('LocationService', () => {
|
|
|
110
111
|
})
|
|
111
112
|
|
|
112
113
|
it('Should return the default value, if not present in the query string', async () => {
|
|
113
|
-
await usingAsync(
|
|
114
|
-
const service = i.
|
|
114
|
+
await usingAsync(createInjector(), async (i) => {
|
|
115
|
+
const service = i.get(LocationService)
|
|
115
116
|
const testSearchParam = service.useSearchParam('test', { value: 'foo' })
|
|
116
117
|
expect(testSearchParam.getValue()).toEqual({ value: 'foo' })
|
|
117
118
|
})
|
|
118
119
|
})
|
|
119
120
|
|
|
120
121
|
it('Should return the value from the query string', async () => {
|
|
121
|
-
await usingAsync(
|
|
122
|
-
const service = i.
|
|
122
|
+
await usingAsync(createInjector(), async (i) => {
|
|
123
|
+
const service = i.get(LocationService)
|
|
123
124
|
history.pushState(null, '', `/loc1?test=${serializeValue(1)}`)
|
|
124
125
|
const testSearchParam = service.useSearchParam('test', 123)
|
|
125
126
|
expect(testSearchParam.getValue()).toBe(1)
|
|
@@ -127,8 +128,8 @@ describe('LocationService', () => {
|
|
|
127
128
|
})
|
|
128
129
|
|
|
129
130
|
it('should update the observable value on push / replace states', async () => {
|
|
130
|
-
await usingAsync(
|
|
131
|
-
const service = i.
|
|
131
|
+
await usingAsync(createInjector(), async (i) => {
|
|
132
|
+
const service = i.get(LocationService)
|
|
132
133
|
history.pushState(null, '', `/loc1?test=${serializeValue(1)}`)
|
|
133
134
|
const testSearchParam = service.useSearchParam('test', 234)
|
|
134
135
|
expect(testSearchParam.getValue()).toBe(1)
|
|
@@ -138,8 +139,8 @@ describe('LocationService', () => {
|
|
|
138
139
|
})
|
|
139
140
|
|
|
140
141
|
it('Should update the URL based on search value change', async () => {
|
|
141
|
-
await usingAsync(
|
|
142
|
-
const service = i.
|
|
142
|
+
await usingAsync(createInjector(), async (i) => {
|
|
143
|
+
const service = i.get(LocationService)
|
|
143
144
|
history.pushState(null, '', `/loc1?test=${serializeValue('2')}`)
|
|
144
145
|
const testSearchParam = service.useSearchParam('test', '')
|
|
145
146
|
testSearchParam.setValue('2')
|
|
@@ -147,25 +148,28 @@ describe('LocationService', () => {
|
|
|
147
148
|
})
|
|
148
149
|
})
|
|
149
150
|
|
|
150
|
-
it('Should throw
|
|
151
|
-
await usingAsync(
|
|
151
|
+
it('Should throw when called after LocationService has been resolved', async () => {
|
|
152
|
+
await usingAsync(createInjector(), async (i) => {
|
|
152
153
|
const customSerializer = vi.fn((value: any) => serializeToQueryString(value))
|
|
153
154
|
const customDeserializer = vi.fn((value: string) => deserializeQueryString(value))
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
|
|
156
|
+
// Eagerly resolve once so the service patches history / adds listeners.
|
|
157
|
+
i.get(LocationService)
|
|
158
|
+
|
|
159
|
+
expect(() => useCustomSearchStateSerializer(i, customSerializer, customDeserializer)).toThrow(
|
|
160
|
+
/before LocationService is resolved/,
|
|
157
161
|
)
|
|
158
162
|
})
|
|
159
163
|
})
|
|
160
164
|
|
|
161
165
|
it('Should use custom serializer and deserializer', async () => {
|
|
162
|
-
await usingAsync(
|
|
166
|
+
await usingAsync(createInjector(), async (i) => {
|
|
163
167
|
const customSerializer = vi.fn((value: any) => serializeToQueryString(value))
|
|
164
168
|
const customDeserializer = vi.fn((value: string) => deserializeQueryString(value))
|
|
165
169
|
|
|
166
170
|
useCustomSearchStateSerializer(i, customSerializer, customDeserializer)
|
|
167
171
|
|
|
168
|
-
const locationService = i.
|
|
172
|
+
const locationService = i.get(LocationService)
|
|
169
173
|
const testSearchParam = locationService.useSearchParam('test', { value: 'foo' })
|
|
170
174
|
|
|
171
175
|
testSearchParam.setValue({ value: 'bar' })
|