vanilla-jui 1.7.7 → 1.7.8

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-jui",
3
- "version": "1.7.7",
3
+ "version": "1.7.8",
4
4
  "private": false,
5
5
  "description": "A lightweight, reactive, and fully customizable vanilla JS UI library.",
6
6
  "keywords": [
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  For,
3
+ Show,
3
4
  createDeepStore,
4
5
  createEffect,
5
6
  createMemo,
6
7
  flushSync,
8
+ insert,
7
9
  jsx,
8
10
  untrack,
9
11
  } from 'vanilla-signal';
@@ -27,6 +29,16 @@ interface TocClassNames {
27
29
 
28
30
  type TocClassNameConfig = Partial<TocClassNames>;
29
31
 
32
+ interface TocIndicatorConfig {
33
+ width: string;
34
+ heightRatio: number;
35
+ radius: string;
36
+ left: string;
37
+ color: string;
38
+ }
39
+
40
+ type TocIndicatorConfigInput = Partial<TocIndicatorConfig>;
41
+
30
42
  export interface TocItem {
31
43
  id: string;
32
44
  text: string;
@@ -52,8 +64,7 @@ interface TocProps extends Record<string, unknown> {
52
64
  offset?: number;
53
65
  reactive?: boolean;
54
66
  title?: boolean;
55
- indicatorWidth?: string;
56
- indicatorHeightRatio?: number;
67
+ indicator?: TocIndicatorConfigInput;
57
68
  className?: TocClassNameConfig;
58
69
  onChange?:
59
70
  | ((item: TocItem | null, index: number, toc: TocInstance) => void)
@@ -66,8 +77,7 @@ interface ResolvedTocProps extends Record<string, unknown> {
66
77
  offset: number;
67
78
  reactive: boolean;
68
79
  title: boolean;
69
- indicatorWidth: string;
70
- indicatorHeightRatio: number;
80
+ indicator: TocIndicatorConfig;
71
81
  className: TocClassNames;
72
82
  onChange:
73
83
  | ((item: TocItem | null, index: number, toc: TocInstance) => void)
@@ -119,6 +129,14 @@ const DEFAULT_CLASS_NAMES: TocClassNames = {
119
129
  indicator: 'toc-indicator',
120
130
  };
121
131
 
132
+ const DEFAULT_INDICATOR_CONFIG: TocIndicatorConfig = {
133
+ width: '2px',
134
+ heightRatio: 5,
135
+ radius: '2px',
136
+ left: '0',
137
+ color: 'var(--tone-solid)',
138
+ };
139
+
122
140
  const TOC_PROPS_SCHEMA = {
123
141
  target: { default: '.j-content' },
124
142
  headings: { default: 'h2, h3', type: 'string' },
@@ -129,12 +147,22 @@ const TOC_PROPS_SCHEMA = {
129
147
  },
130
148
  reactive: { default: false, type: 'boolean' },
131
149
  title: { default: false, type: 'boolean' },
132
- indicatorWidth: { default: '2px', type: 'string' },
133
- indicatorHeightRatio: {
134
- default: 5,
135
- type: 'number',
136
- min: 1,
137
- max: 10,
150
+ indicator: {
151
+ default: DEFAULT_INDICATOR_CONFIG,
152
+ type: 'plainObject',
153
+ merge: 'shallow',
154
+ schema: {
155
+ width: { default: DEFAULT_INDICATOR_CONFIG.width, type: 'string' },
156
+ heightRatio: {
157
+ default: DEFAULT_INDICATOR_CONFIG.heightRatio,
158
+ type: 'number',
159
+ min: 1,
160
+ max: 10,
161
+ },
162
+ radius: { default: DEFAULT_INDICATOR_CONFIG.radius, type: 'string' },
163
+ left: { default: DEFAULT_INDICATOR_CONFIG.left, type: 'string' },
164
+ color: { default: DEFAULT_INDICATOR_CONFIG.color, type: 'string' },
165
+ },
138
166
  },
139
167
  className: {
140
168
  default: DEFAULT_CLASS_NAMES,
@@ -159,8 +187,7 @@ function normalizeProps(input: TocProps): ResolvedTocProps {
159
187
  offset: props.offset as number,
160
188
  reactive: props.reactive as boolean,
161
189
  title: props.title as boolean,
162
- indicatorWidth: props.indicatorWidth as string,
163
- indicatorHeightRatio: props.indicatorHeightRatio as number,
190
+ indicator: props.indicator as TocIndicatorConfig,
164
191
  className: props.className as TocClassNames,
165
192
  onChange: props.onChange as ResolvedTocProps['onChange'],
166
193
  };
@@ -238,7 +265,10 @@ export function createToc(props: TocProps = {}): TocInstance {
238
265
  let activeLinkId: string | null = null;
239
266
  let listHeight = 0;
240
267
  const linkRefs = new Map<string, HTMLElement>();
241
- const renderableItems = createMemo(() => state.items);
268
+ const getItems = (): TocItem[] =>
269
+ Array.isArray(state.items) ? state.items : [];
270
+ const hasItems = (): boolean => getItems().length > 0;
271
+ const renderableItems = createMemo(getItems);
242
272
 
243
273
  const setIndicator = (indicator: TocIndicator): void => {
244
274
  if (
@@ -307,7 +337,7 @@ export function createToc(props: TocProps = {}): TocInstance {
307
337
  const linkRect = link.getBoundingClientRect();
308
338
  const linkHeight = link.offsetHeight || linkRect.height;
309
339
  const linkTop = link.offsetTop;
310
- const height = (linkHeight * settings.indicatorHeightRatio) / 10;
340
+ const height = (linkHeight * settings.indicator.heightRatio) / 10;
311
341
  const top = linkTop + (linkHeight - height) / 2;
312
342
  const distance = state.indicator.visible
313
343
  ? Math.abs(top - state.indicator.top)
@@ -333,15 +363,16 @@ export function createToc(props: TocProps = {}): TocInstance {
333
363
  };
334
364
 
335
365
  const syncListLayout = (): void => {
336
- if (!toc.runtime.built || !listElement || state.items.length === 0) {
366
+ const items = getItems();
367
+ if (!toc.runtime.built || !listElement || items.length === 0) {
337
368
  listHeight = 0;
338
369
  setIndicator(hiddenIndicator());
339
370
  return;
340
371
  }
341
- pruneLinkRefs(state.items);
372
+ pruneLinkRefs(items);
342
373
  const listRect = listElement.getBoundingClientRect();
343
374
  listHeight = listElement.offsetHeight || listRect.height;
344
- const current = state.items[state.current.index] || null;
375
+ const current = items[state.current.index] || null;
345
376
  updateIndicatorForItem(current);
346
377
  };
347
378
 
@@ -400,13 +431,14 @@ export function createToc(props: TocProps = {}): TocInstance {
400
431
  };
401
432
 
402
433
  const setActive = (index: number): number => {
434
+ const items = getItems();
403
435
  if (index === state.current.index) {
404
- const current = state.items[index] || null;
436
+ const current = items[index] || null;
405
437
  const duration = updateIndicatorForItem(current);
406
438
  if (duration < 0) scheduleListLayoutSync();
407
439
  return duration;
408
440
  }
409
- const current = state.items[index] || null;
441
+ const current = items[index] || null;
410
442
  flushSync(() => {
411
443
  state.current = { index, item: current };
412
444
  });
@@ -487,13 +519,14 @@ export function createToc(props: TocProps = {}): TocInstance {
487
519
  };
488
520
 
489
521
  const activate = (index: number): TocInstance => {
522
+ const items = getItems();
490
523
  if (
491
524
  toc.runtime.built &&
492
525
  Number.isInteger(index) &&
493
526
  index >= 0 &&
494
- index < state.items.length
527
+ index < items.length
495
528
  ) {
496
- scrollToItem(state.items[index], { activeIndex: index });
529
+ scrollToItem(items[index], { activeIndex: index });
497
530
  }
498
531
  return toc;
499
532
  };
@@ -505,80 +538,95 @@ export function createToc(props: TocProps = {}): TocInstance {
505
538
  actions: { activate },
506
539
  view: () => {
507
540
  createEffect(() => {
508
- const items = state.items;
541
+ const items = getItems();
509
542
  void items.map((item) => item.id).join('\0');
510
543
  untrack(() => {
511
544
  pruneLinkRefs(items);
545
+ if (items.length === 0) {
546
+ listElement = null;
547
+ listHeight = 0;
548
+ linkRefs.clear();
549
+ syncActiveMarker(null);
550
+ setIndicator(hiddenIndicator());
551
+ }
512
552
  scheduleListLayoutSync();
513
553
  });
514
554
  });
515
- const titleElement = settings.title
516
- ? jsx('div', {
517
- className: settings.className.title,
518
- 'data-toc-title': '',
519
- children: translate('toc'),
520
- })
521
- : null;
522
- const listView = jsx('div', {
523
- ref: (element: HTMLElement) => {
524
- listElement = element;
525
- scheduleListLayoutSync();
526
- },
527
- className: settings.className.list,
528
- 'data-toc-list': 'root',
529
- children: [
530
- For({
531
- each: renderableItems,
532
- key: (item: TocItem) => item.id,
533
- children: (
534
- itemAccessor: () => TocItem,
535
- indexAccessor: () => number
536
- ) =>
537
- jsx('a', {
538
- ref: (element: HTMLElement) =>
539
- bindLinkRef(itemAccessor(), element),
540
- className: settings.className.link,
541
- href: () => `#${itemAccessor().id}`,
542
- 'data-toc-link': () => String(indexAccessor()),
543
- 'data-toc-target': () => itemAccessor().id,
544
- 'data-toc-level': () => String(itemAccessor().level),
545
- onClick: (event: Event) => {
546
- event.preventDefault();
547
- const item = itemAccessor();
548
- scrollToItem(item, {
549
- activeIndex: state.items.findIndex(
550
- (current) => current.id === item.id
551
- ),
552
- updateHash: true,
553
- });
554
- },
555
- children: () => itemAccessor().text,
556
- }),
557
- }),
558
- For({
559
- each: () => (state.items.length > 0 ? [true] : []),
560
- key: () => 'toc-indicator',
561
- children: () =>
562
- jsx('div', {
563
- className: settings.className.indicator,
564
- 'data-toc-indicator': '',
565
- hidden: () => !state.indicator.visible,
566
- style: () => ({
567
- top: `${state.indicator.top}px`,
568
- width: settings.indicatorWidth,
569
- height: `${state.indicator.height}px`,
570
- transitionDuration: `${state.indicator.duration}ms`,
571
- transitionTimingFunction: state.indicator.timingFunction,
572
- }),
573
- }),
574
- }),
575
- ],
576
- });
577
- return jsx('nav', {
555
+ const root = jsx('nav', {
578
556
  className: settings.className.toc,
579
557
  'data-toc': 'root',
580
- children: settings.title ? [titleElement, listView] : listView,
581
558
  }) as HTMLElement;
559
+ insert(
560
+ root,
561
+ Show({
562
+ when: hasItems,
563
+ children: () => {
564
+ const titleElement = settings.title
565
+ ? jsx('div', {
566
+ className: settings.className.title,
567
+ 'data-toc-title': '',
568
+ children: translate('toc'),
569
+ })
570
+ : null;
571
+ const listView = jsx('div', {
572
+ ref: (element: HTMLElement) => {
573
+ listElement = element;
574
+ scheduleListLayoutSync();
575
+ },
576
+ className: settings.className.list,
577
+ 'data-toc-list': 'root',
578
+ children: For({
579
+ each: renderableItems,
580
+ key: (item: TocItem) => item.id,
581
+ children: (itemAccessor: () => TocItem) =>
582
+ jsx('a', {
583
+ ref: (element: HTMLElement) =>
584
+ bindLinkRef(itemAccessor(), element),
585
+ className: settings.className.link,
586
+ href: () => `#${itemAccessor().id}`,
587
+ 'data-toc-level': () => String(itemAccessor().level),
588
+ onClick: (event: Event) => {
589
+ event.preventDefault();
590
+ const item = itemAccessor();
591
+ scrollToItem(item, {
592
+ activeIndex: getItems().findIndex(
593
+ (current) => current.id === item.id
594
+ ),
595
+ updateHash: true,
596
+ });
597
+ },
598
+ children: () => itemAccessor().text,
599
+ }),
600
+ }),
601
+ }) as HTMLElement;
602
+ insert(
603
+ listView,
604
+ Show({
605
+ when: hasItems,
606
+ children: () =>
607
+ jsx('div', {
608
+ className: settings.className.indicator,
609
+ 'data-toc-indicator': '',
610
+ hidden: () => !state.indicator.visible,
611
+ style: () => ({
612
+ pointerEvents: 'none',
613
+ position: 'absolute',
614
+ left: settings.indicator.left,
615
+ top: `${state.indicator.top}px`,
616
+ width: settings.indicator.width,
617
+ height: `${state.indicator.height}px`,
618
+ transition: `top ${state.indicator.duration}ms ${state.indicator.timingFunction}`,
619
+ borderRadius: settings.indicator.radius,
620
+ backgroundColor: settings.indicator.color,
621
+ }),
622
+ }),
623
+ })
624
+ );
625
+ return settings.title ? [titleElement, listView] : listView;
626
+ },
627
+ })
628
+ );
629
+ return root;
582
630
  },
583
631
  onBuild(context) {
584
632
  target = requireContainer(settings.target, 'Toc.target');
@@ -1,16 +1,12 @@
1
1
  @layer themes {
2
- .j-toc {
3
- width: 100%;
4
- padding-left: 0.75rem;
5
- }
6
-
7
2
  .j-toc .toc-title {
8
3
  color: var(--tone-caption);
9
4
  margin-bottom: 4px;
10
5
  }
11
6
 
12
7
  .j-toc .toc-link {
13
- padding-block: 0.375rem;
8
+ padding-top: 0.375rem;
9
+ padding-bottom: 0.375rem;
14
10
  color: var(--tone-caption);
15
11
  font-size: var(--font-size-sm);
16
12
  text-decoration: none;
@@ -22,19 +18,11 @@
22
18
  color: var(--tone-heading);
23
19
  }
24
20
 
21
+ .j-toc:has(.toc-link),
25
22
  .j-toc .toc-link[data-toc-level='3'],
26
23
  .j-toc .toc-link[data-toc-level='4'],
27
24
  .j-toc .toc-link[data-toc-level='5'],
28
25
  .j-toc .toc-link[data-toc-level='6'] {
29
26
  padding-left: 0.75rem;
30
27
  }
31
-
32
- .j-toc .toc-indicator {
33
- position: absolute;
34
- left: 0;
35
- background-color: var(--tone-solid);
36
- transition-property: top;
37
- pointer-events: none;
38
- border-radius: 2px;
39
- }
40
28
  }