sygnal 6.6.0 → 7.1.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.
Files changed (91) hide show
  1. package/CHANGELOG.md +258 -0
  2. package/README.md +2 -0
  3. package/dist/astro/index.cjs.js +32 -9
  4. package/dist/astro/index.cjs.js.map +1 -1
  5. package/dist/astro/index.mjs +32 -9
  6. package/dist/astro/index.mjs.map +1 -1
  7. package/dist/astro/server.cjs.js +32 -43
  8. package/dist/astro/server.cjs.js.map +1 -1
  9. package/dist/astro/server.mjs +32 -43
  10. package/dist/astro/server.mjs.map +1 -1
  11. package/dist/devtools.cjs.js.map +1 -1
  12. package/dist/devtools.esm.js.map +1 -1
  13. package/dist/diagnostics.cjs.js +154 -6
  14. package/dist/diagnostics.cjs.js.map +1 -1
  15. package/dist/diagnostics.esm.js +153 -7
  16. package/dist/diagnostics.esm.js.map +1 -1
  17. package/dist/guide/accessibility.md +3 -1
  18. package/dist/guide/ai-chat.md +6 -6
  19. package/dist/guide/behaviors.md +1 -1
  20. package/dist/guide/element-commands.md +4 -2
  21. package/dist/guide/forms-reference.md +89 -3
  22. package/dist/guide/forms.md +8 -8
  23. package/dist/guide/http.md +77 -7
  24. package/dist/guide/migrating-to-7.md +314 -0
  25. package/dist/guide/recipes/ai-summarize.md +2 -1
  26. package/dist/guide/recipes/i18n.md +1 -1
  27. package/dist/guide/resources.md +2 -0
  28. package/dist/guide/timers.md +3 -3
  29. package/dist/guide/ui/tabs.md +5 -4
  30. package/dist/guide/ui/tooltip.md +24 -0
  31. package/dist/index.cjs.js +954 -388
  32. package/dist/index.cjs.js.map +1 -1
  33. package/dist/index.d.ts +300 -46
  34. package/dist/index.esm.js +953 -389
  35. package/dist/index.esm.js.map +1 -1
  36. package/dist/sygnal.min.js +1 -1
  37. package/dist/sygnal.min.js.map +1 -1
  38. package/dist/ui.cjs.js +96 -11
  39. package/dist/ui.cjs.js.map +1 -1
  40. package/dist/ui.esm.js +96 -12
  41. package/dist/ui.esm.js.map +1 -1
  42. package/dist/vike/config/package.json +1 -1
  43. package/dist/vite/plugin.cjs.js +19 -9
  44. package/dist/vite/plugin.cjs.js.map +1 -1
  45. package/dist/vite/plugin.mjs +19 -9
  46. package/dist/vite/plugin.mjs.map +1 -1
  47. package/llms.txt +23 -22
  48. package/package.json +1 -1
  49. package/src/astro/index.d.ts +8 -0
  50. package/src/astro/index.ts +18 -0
  51. package/src/core/actions.ts +28 -21
  52. package/src/core/define.ts +2 -0
  53. package/src/core/hooks.ts +2 -2
  54. package/src/core/hosts/collection.ts +4 -3
  55. package/src/core/hosts/switchable.ts +26 -8
  56. package/src/core/instance.ts +66 -17
  57. package/src/core/runtime.ts +5 -1
  58. package/src/core/statics.ts +5 -5
  59. package/src/extra/ai/chat/behavior.ts +3 -3
  60. package/src/extra/ai/commandBar.ts +3 -3
  61. package/src/extra/ai/compose/behavior.ts +3 -3
  62. package/src/extra/ai/transports/shared.ts +10 -2
  63. package/src/extra/behaviors.ts +24 -3
  64. package/src/extra/diagnostics/checks/fetch.ts +21 -1
  65. package/src/extra/diagnostics/checks/forms.ts +4 -3
  66. package/src/extra/diagnostics/checks/index.ts +1 -0
  67. package/src/extra/diagnostics/checks/next.ts +49 -3
  68. package/src/extra/diagnostics/checks/props.ts +6 -1
  69. package/src/extra/diagnostics/checks/public.d.ts +12 -0
  70. package/src/extra/diagnostics/checks/timingAudit.ts +47 -0
  71. package/src/extra/diagnostics/codes.ts +24 -0
  72. package/src/extra/fetchDriver.ts +115 -7
  73. package/src/extra/fetchEventSource.ts +117 -0
  74. package/src/extra/form.ts +68 -26
  75. package/src/extra/formHelpers.ts +3 -1
  76. package/src/extra/lens.ts +3 -2
  77. package/src/extra/queryCache.ts +4 -3
  78. package/src/extra/socketDriver.ts +60 -9
  79. package/src/extra/ssr.ts +30 -41
  80. package/src/extra/testing.ts +100 -18
  81. package/src/extra/timers.ts +5 -2
  82. package/src/index.d.ts +299 -45
  83. package/src/index.ts +3 -0
  84. package/src/shared.ts +3 -0
  85. package/src/ui/dialog.ts +3 -2
  86. package/src/ui/tooltips.ts +92 -0
  87. package/src/ui.d.ts +35 -0
  88. package/src/ui.ts +1 -0
  89. package/src/vite/graph.ts +11 -3
  90. package/src/vite/plugin.d.ts +7 -0
  91. package/src/vite/plugin.ts +10 -5
@@ -0,0 +1,92 @@
1
+ /*
2
+ * PLAN-11 T-1: Tooltips, one behavior for every tooltip of a component (glossary terms in a
3
+ * paragraph, icon buttons in a toolbar): one state slice and two timers however many triggers
4
+ * there are, instead of a tooltip() per trigger.
5
+ *
6
+ * Article.uses = { tips: tooltips({ trigger: '.term', tip: '.term-tip' }) }
7
+ *
8
+ * Each trigger names its tip with aria-describedby (the tip's id); the tips are `role="tooltip"
9
+ * popover="manual"` elements placed by CSS anchor positioning, as for tooltip(). One tip shows at
10
+ * a time. Moving from one trigger to another while a tip is shown switches at once (no second
11
+ * show delay), as tooltip groups do.
12
+ *
13
+ * Options: `trigger`, `tip` (required selectors matching every trigger / every tip),
14
+ * `showDelay` (500 ms), `hideDelay` (100 ms).
15
+ * State: { id (the tip of the trigger or tip last entered), open (the id of the tip shown),
16
+ * pending: null | 'show' | 'hide', hover, focus }. Actions: ENTER / LEAVE ({ via: 'hover' |
17
+ * 'focus', id }), SHOW / HIDE (the timers), ESCAPE, TOGGLED ({ id, open }). Timers
18
+ * '<key>.show' / '<key>.hide' as tooltip()'s. Needs makeTimerDriver() (tests: fake timers).
19
+ * Same rules as tooltip(): pointer and focus tracked apart, touch ignored, hovering the tip keeps
20
+ * it open (WCAG 1.4.13), Escape listened for only while a tip is shown or about to be.
21
+ */
22
+ import {ABORT, defineBehavior, dropRepeats, xs} from '../index'
23
+
24
+ const mouse = (e: any) => e?.pointerType != 'touch'
25
+ const el = (e: any) => e?.ownerTarget || e?.currentTarget || e?.target
26
+ // the trigger's tip id (its aria-describedby: the first id when it lists several), or the tip's own
27
+ const tipOf = (e: any) => String(el(e)?.getAttribute?.('aria-describedby') || '').trim().split(/\s+/)[0] || null
28
+ const idOf = (e: any) => el(e)?.id || null
29
+ const at = (id: string) => `[id="${id.replace(/["\\]/g, '\\$&')}"]`
30
+
31
+ const base = /*#__PURE__*/ defineBehavior({
32
+ initialState: {id: null as null | string, open: null as null | string, pending: null as null | string, hover: false, focus: false},
33
+ timers: (s: any, o: any) => ({
34
+ show: s?.pending == 'show' && {after: o.showDelay ?? 500, action: 'SHOW'},
35
+ hide: s?.pending == 'hide' && {after: o.hideDelay ?? 100, action: 'HIDE'},
36
+ }),
37
+ intent: ({DOM, STATE}: any, {trigger, tip}: any) => ({
38
+ ENTER: xs.merge(
39
+ DOM.pointerenter(trigger).filter(mouse).map((e: any) => ({via: 'hover', id: tipOf(e)})),
40
+ DOM.pointerenter(tip).filter(mouse).map((e: any) => ({via: 'hover', id: idOf(e)})),
41
+ DOM.focus(trigger).map((e: any) => ({via: 'focus', id: tipOf(e)})),
42
+ ).filter((x: any) => !!x.id),
43
+ LEAVE: xs.merge(
44
+ DOM.pointerleave(trigger).filter(mouse).map((e: any) => ({via: 'hover', id: tipOf(e)})),
45
+ DOM.pointerleave(tip).filter(mouse).map((e: any) => ({via: 'hover', id: idOf(e)})),
46
+ DOM.blur(trigger).map((e: any) => ({via: 'focus', id: tipOf(e)})),
47
+ ).filter((x: any) => !!x.id),
48
+ ESCAPE: (STATE ? STATE.stream.map((s: any) => !!(s?.open || s?.pending)).compose(dropRepeats()) : xs.of(true))
49
+ .map((on: boolean) => (on ? DOM.keydown('document').key().filter((k: string) => k == 'Escape') : xs.never()))
50
+ .flatten(),
51
+ TOGGLED: DOM.toggle(tip).map((e: any) => ({id: idOf(e), open: e.newState == 'open'})).filter((x: any) => !!x.id),
52
+ }),
53
+ model: {
54
+ ENTER: {
55
+ STATE: (s: any, {via, id}: any) => {
56
+ const k = via == 'focus' ? 'focus' : 'hover'
57
+ // another trigger: it takes over (a tip already shown switches at once, in ELEMENT)
58
+ if (id != s.id) return {...s, id, hover: k == 'hover', focus: k == 'focus', pending: s.open ? null : 'show'}
59
+ const pending = s.open == id ? null : 'show'
60
+ return s[k] && s.pending == pending ? ABORT : {...s, [k]: true, pending}
61
+ },
62
+ // 7.0: outputs get the updated slice; "another trigger than before" reads the previous one
63
+ ELEMENT: (s: any, {id}: any, _n: any, p: any, _o: any, k: string) => {
64
+ const w = p?.previousState ? p.previousState[k] : s
65
+ return id != w.id && w.open && w.open != id ? [{hidePopover: at(w.open)}, {showPopover: at(id)}] : ABORT
66
+ },
67
+ },
68
+ LEAVE: (s: any, {via, id}: any) => {
69
+ if (id != s.id) return ABORT
70
+ const k = via == 'focus' ? 'focus' : 'hover'
71
+ const n = {...s, [k]: false}
72
+ const pending = n.hover || n.focus ? s.pending : s.open ? 'hide' : null
73
+ return !s[k] && s.pending == pending ? ABORT : {...n, pending}
74
+ },
75
+ SHOW: {
76
+ STATE: (s: any) => ({...s, pending: null}),
77
+ ELEMENT: (s: any) => (!s.id || s.open == s.id ? ABORT : s.open ? [{hidePopover: at(s.open)}, {showPopover: at(s.id)}] : {showPopover: at(s.id)}),
78
+ },
79
+ HIDE: {
80
+ STATE: (s: any) => ({...s, pending: null}),
81
+ ELEMENT: (s: any) => (s.open ? {hidePopover: at(s.open)} : ABORT),
82
+ },
83
+ ESCAPE: {
84
+ STATE: (s: any) => (s.open || s.pending ? {...s, pending: null} : ABORT),
85
+ ELEMENT: (s: any) => (s.open ? {hidePopover: at(s.open)} : ABORT),
86
+ },
87
+ TOGGLED: (s: any, {id, open}: any) => (open ? (s.open == id ? ABORT : {...s, open: id}) : s.open == id ? {...s, open: null} : ABORT),
88
+ },
89
+ })
90
+
91
+ /** Every tooltip of a component as one behavior: `uses = { tips: tooltips({ trigger: '.term', tip: '.term-tip' }) }`. */
92
+ export const tooltips = (options: any = {}): any => base({...options, id: null, open: null, pending: null, hover: false, focus: false})
package/src/ui.d.ts CHANGED
@@ -75,6 +75,41 @@ export interface TooltipActions { ENTER: 'hover' | 'focus' | undefined; LEAVE: '
75
75
  /** A tooltip: `uses = { tip: tooltip({ trigger: '.save', tip: '.save-tip' }) }`. Needs makeTimerDriver(). */
76
76
  export function tooltip(options: TooltipOptions): Behavior<TooltipState, TooltipActions, {}, TooltipOptions>
77
77
 
78
+ // ── Tooltips (PLAN-11) ────────────────────────────────────────────────
79
+ export interface TooltipsState {
80
+ /** The tip id of the trigger (or tip) last entered */
81
+ id: string | null
82
+ /** The id of the tip shown, or null */
83
+ open: string | null
84
+ pending: null | 'show' | 'hide'
85
+ hover: boolean
86
+ focus: boolean
87
+ }
88
+ export interface TooltipsOptions {
89
+ /** Every trigger (a selector matching them all); each names its tip with aria-describedby */
90
+ trigger: BehaviorTarget
91
+ /** Every tip: role="tooltip" popover="manual" with an id, placed with CSS anchor positioning */
92
+ tip: BehaviorTarget
93
+ /** ms before a tip shows (default 500); moving to another trigger while one shows switches at once */
94
+ showDelay?: number
95
+ /** ms before it hides (default 100) */
96
+ hideDelay?: number
97
+ }
98
+ export interface TooltipsActions {
99
+ ENTER: { via: 'hover' | 'focus'; id: string }
100
+ LEAVE: { via: 'hover' | 'focus'; id: string }
101
+ SHOW: any
102
+ HIDE: any
103
+ ESCAPE: string
104
+ TOGGLED: { id: string; open: boolean }
105
+ }
106
+ /**
107
+ * PLAN-11 (6.7): every tooltip of a component as one behavior (one state slice, two timers):
108
+ * `uses = { tips: tooltips({ trigger: '.term', tip: '.term-tip' }) }`, each trigger with
109
+ * `aria-describedby` naming its tip's id. One tip shows at a time. Needs makeTimerDriver().
110
+ */
111
+ export function tooltips(options: TooltipsOptions): Behavior<TooltipsState, TooltipsActions, {}, TooltipsOptions>
112
+
78
113
  // ── Tabs ──────────────────────────────────────────────────────────────
79
114
  export interface TabsState {
80
115
  /** The ids' prefix (the key in `uses` unless the `id` option names it) */
package/src/ui.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  export {dialog} from './ui/dialog'
10
10
  export {popover} from './ui/popover'
11
11
  export {tooltip} from './ui/tooltip'
12
+ export {tooltips} from './ui/tooltips'
12
13
  export {tabs, tabsAttrs} from './ui/tabs'
13
14
  export {accordion, accordionAttrs} from './ui/accordion'
14
15
  export {disclosure, disclosureAttrs} from './ui/disclosure'
package/src/vite/graph.ts CHANGED
@@ -232,15 +232,23 @@ export function graphMiddleware(server: any, root: string, engine: Promise<LiveE
232
232
  }
233
233
  }
234
234
 
235
- /** Adds the page's URL under Vite's own when the dev server prints them. */
236
- export function printGraphUrl(server: any) {
235
+ /** The banner line when the project has no sygnal-check with the graph page (it used to print nothing). */
236
+ export const GRAPH_MISSING_LINE = ' ➜ Sygnal graph: needs sygnal-check 0.10.0 or later in this project (npm i -D sygnal-check)'
237
+
238
+ /**
239
+ * Adds the page's URL under Vite's own when the dev server prints them, or, when the project's
240
+ * sygnal-check is missing or too old for the page (`available` false), a line saying what to install.
241
+ */
242
+ export function printGraphUrl(server: any, available = true) {
237
243
  const print = server?.printUrls
238
244
  if (typeof print !== 'function') return
239
245
  server.printUrls = function (this: any, ...args: any[]) {
240
246
  const out = print.apply(this, args)
241
247
  try {
248
+ const logger = server.config?.logger || console
242
249
  const local = server.resolvedUrls?.local?.[0]
243
- if (local) (server.config?.logger || console).info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`)
250
+ if (!available) logger.info(GRAPH_MISSING_LINE)
251
+ else if (local) logger.info(` ➜ Sygnal graph: ${new URL(local).origin}${GRAPH_PATH}`)
244
252
  } catch (_) {}
245
253
  return out
246
254
  }
@@ -12,6 +12,13 @@ export interface DiagnosticsPluginOptions {
12
12
  * @default false
13
13
  */
14
14
  strict?: boolean
15
+ /**
16
+ * 7.0 timing audit (SYG257, info): the dev snippet sets `globalThis.__SYGNAL_TIMING_AUDIT__ =
17
+ * true`, and each output sink that reads a top-level state field its action's STATE changed is
18
+ * reported (since 7.0 it gets the new value; 6.x gave it the old one). For migrating 6.x code.
19
+ * @default false
20
+ */
21
+ timingAudit?: boolean
15
22
  /** Diagnostic codes to drop, e.g. ['SYG105'] (runtime and static) */
16
23
  ignore?: string[]
17
24
  }
@@ -130,7 +130,8 @@
130
130
  * skipped silently when missing; a sygnal-check without the page keeps the checker as in 5)
131
131
  * builds it with createLiveProject on start and after each change: one build gives the page,
132
132
  * the check overlay / console findings of 5, and the component stamps. Its URL is printed
133
- * under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"). The modules of the
133
+ * under Vite's ("➜ Sygnal graph: http://localhost:5173/__sygnal/graph"; without a sygnal-check
134
+ * 0.10.0+ in the project, a line saying so instead). The modules of the
134
135
  * scanned files get a tail setting a non-enumerable `__sygnalKey` (the static 'file#name'
135
136
  * key, D521) on each top-level component function, which the runtime inspect() reports as
136
137
  * `key`; with DevTools on, the dev entries also import 'virtual:sygnal/graph'
@@ -191,6 +192,8 @@ export interface DiagnosticsPluginOptions {
191
192
  mode?: DiagnosticsMode
192
193
  /** Strict (canonical-form, SYG5xx) runtime checks: sets globalThis.__SYGNAL_STRICT__. @default false */
193
194
  strict?: boolean
195
+ /** 7.0 timing audit (SYG257, info): sets globalThis.__SYGNAL_TIMING_AUDIT__. @default false */
196
+ timingAudit?: boolean
194
197
  /** Diagnostic codes to drop, e.g. ['SYG105'] */
195
198
  ignore?: string[]
196
199
  }
@@ -343,7 +346,7 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
343
346
  // Files whose `import ... from 'sygnal'` resolves to the run() wrapper
344
347
  const runtimeImporters = new Set<string>()
345
348
  // Statements run before the app starts: dev flag (+ strict flag)
346
- const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '')
349
+ const flags = DEV_FLAG + (diagnostics.strict ? STRICT_FLAG : '') + (diagnostics.timingAudit ? AUDIT_FLAG : '')
347
350
  const devImports = `import 'sygnal/diagnostics';import '${DEV_CLIENT}';`
348
351
  // D77: the DevTools bridge, dev only. First, so the diagnostics entry finds it.
349
352
  // E-1: the MCP endpoint needs the bridge, so `mcp` turns it on
@@ -535,7 +538,7 @@ export default function sygnal(options: SygnalPluginOptions = {}) {
535
538
  // W-2 (D543): the page and its stream, behind the MCP endpoint's guard; its URL in the banner
536
539
  if (graphOn && server?.middlewares) {
537
540
  server.middlewares.use(graphMiddleware(server, root, engine))
538
- if (graphAvailable(root)) printGraphUrl(server)
541
+ printGraphUrl(server, graphAvailable(root))
539
542
  }
540
543
  },
541
544
 
@@ -790,7 +793,7 @@ function viteMajor(ctx: any, root: string): number {
790
793
  return Infinity
791
794
  }
792
795
 
793
- interface NormalizedDiagnostics { mode: DiagnosticsMode, strict: boolean, ignore: string[] }
796
+ interface NormalizedDiagnostics { mode: DiagnosticsMode, strict: boolean, timingAudit: boolean, ignore: string[] }
794
797
 
795
798
  const MODES: DiagnosticsMode[] = ['off', 'collect', 'warn', 'error']
796
799
 
@@ -800,7 +803,7 @@ function normalizeDiagnostics(value: SygnalPluginOptions['diagnostics']): Normal
800
803
  if (!MODES.includes(mode)) {
801
804
  throw new Error(`[vite-plugin-sygnal] diagnostics mode must be one of ${MODES.map(m => `'${m}'`).join(', ')}; got ${JSON.stringify(mode)}`)
802
805
  }
803
- return { mode, strict: !!o.strict, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] }
806
+ return { mode, strict: !!o.strict, timingAudit: !!o.timingAudit, ignore: Array.isArray(o.ignore) ? o.ignore.map(String) : [] }
804
807
  }
805
808
 
806
809
  /** Module id without a query or hash. */
@@ -1155,6 +1158,8 @@ function startChecker(server: any, root: string, mod: any, setup: CheckSetup, op
1155
1158
 
1156
1159
  const DEV_FLAG = 'if (globalThis.__SYGNAL_DEV__ === undefined) globalThis.__SYGNAL_DEV__ = true;'
1157
1160
  const STRICT_FLAG = 'if (globalThis.__SYGNAL_STRICT__ === undefined) globalThis.__SYGNAL_STRICT__ = true;'
1161
+ // 7.0: the opt-in timing audit (SYG257, 'sygnal/diagnostics' checks/timingAudit.ts)
1162
+ const AUDIT_FLAG = 'if (globalThis.__SYGNAL_TIMING_AUDIT__ === undefined) globalThis.__SYGNAL_TIMING_AUDIT__ = true;'
1158
1163
 
1159
1164
  // A top-level (column 0) run(Component, ...) statement: bare, or declaring a
1160
1165
  // new binding. Assignments to existing variables, nested calls and indented