synthos 0.8.0 → 0.10.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 (368) hide show
  1. package/README.md +1 -1
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1803 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/{oregon_trail.html → oregon_trail/page.html} +16 -30
  27. package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_page.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +15 -12
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_page.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/{us_map.html → us_map/page.html} +193 -192
  39. package/default-pages/{us_map.json → us_map/page.json} +12 -12
  40. package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
  41. package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
  42. package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
  43. package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/nebula-dawn.v2.css +110 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/nebula-dusk.v2.css +104 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/index.d.ts +1 -1
  65. package/dist/agents/index.d.ts.map +1 -1
  66. package/dist/agents/index.js +2 -1
  67. package/dist/agents/index.js.map +1 -1
  68. package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
  69. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
  70. package/dist/agents/openclaw/gatewayManager.js +27 -11
  71. package/dist/agents/openclaw/gatewayManager.js.map +1 -1
  72. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
  73. package/dist/agents/openclaw/openclawProvider.js +2 -4
  74. package/dist/agents/openclaw/openclawProvider.js.map +1 -1
  75. package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
  76. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
  77. package/dist/agents/openclaw/sshTunnelManager.js +31 -12
  78. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
  79. package/dist/builders/anthropic.d.ts +31 -0
  80. package/dist/builders/anthropic.d.ts.map +1 -0
  81. package/dist/builders/anthropic.js +227 -0
  82. package/dist/builders/anthropic.js.map +1 -0
  83. package/dist/builders/fireworksai.d.ts +9 -0
  84. package/dist/builders/fireworksai.d.ts.map +1 -0
  85. package/dist/builders/fireworksai.js +57 -0
  86. package/dist/builders/fireworksai.js.map +1 -0
  87. package/dist/builders/index.d.ts +13 -0
  88. package/dist/builders/index.d.ts.map +1 -0
  89. package/dist/builders/index.js +31 -0
  90. package/dist/builders/index.js.map +1 -0
  91. package/dist/builders/openai.d.ts +8 -0
  92. package/dist/builders/openai.d.ts.map +1 -0
  93. package/dist/builders/openai.js +87 -0
  94. package/dist/builders/openai.js.map +1 -0
  95. package/dist/builders/types.d.ts +54 -0
  96. package/dist/builders/types.d.ts.map +1 -0
  97. package/dist/builders/types.js +211 -0
  98. package/dist/builders/types.js.map +1 -0
  99. package/dist/connectors/index.d.ts +1 -1
  100. package/dist/connectors/index.d.ts.map +1 -1
  101. package/dist/connectors/index.js +3 -2
  102. package/dist/connectors/index.js.map +1 -1
  103. package/dist/connectors/registry.d.ts +2 -1
  104. package/dist/connectors/registry.d.ts.map +1 -1
  105. package/dist/connectors/registry.js +31 -8
  106. package/dist/connectors/registry.js.map +1 -1
  107. package/dist/customizer/Customizer.d.ts +62 -0
  108. package/dist/customizer/Customizer.d.ts.map +1 -0
  109. package/dist/customizer/Customizer.js +134 -0
  110. package/dist/customizer/Customizer.js.map +1 -0
  111. package/dist/customizer/index.d.ts +4 -0
  112. package/dist/customizer/index.d.ts.map +1 -0
  113. package/dist/customizer/index.js +9 -0
  114. package/dist/customizer/index.js.map +1 -0
  115. package/dist/files.d.ts +16 -0
  116. package/dist/files.d.ts.map +1 -1
  117. package/dist/files.js +60 -1
  118. package/dist/files.js.map +1 -1
  119. package/dist/index.d.ts +2 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +2 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/init.d.ts +12 -6
  124. package/dist/init.d.ts.map +1 -1
  125. package/dist/init.js +150 -133
  126. package/dist/init.js.map +1 -1
  127. package/dist/migrations.d.ts.map +1 -1
  128. package/dist/migrations.js +23 -10
  129. package/dist/migrations.js.map +1 -1
  130. package/dist/models/anthropic.d.ts +4 -2
  131. package/dist/models/anthropic.d.ts.map +1 -1
  132. package/dist/models/anthropic.js +33 -6
  133. package/dist/models/anthropic.js.map +1 -1
  134. package/dist/models/fireworksai.d.ts.map +1 -1
  135. package/dist/models/fireworksai.js +9 -1
  136. package/dist/models/fireworksai.js.map +1 -1
  137. package/dist/models/index.d.ts +1 -1
  138. package/dist/models/index.d.ts.map +1 -1
  139. package/dist/models/index.js +2 -1
  140. package/dist/models/index.js.map +1 -1
  141. package/dist/models/openai.d.ts +1 -1
  142. package/dist/models/openai.d.ts.map +1 -1
  143. package/dist/models/openai.js +24 -3
  144. package/dist/models/openai.js.map +1 -1
  145. package/dist/models/types.d.ts +20 -1
  146. package/dist/models/types.d.ts.map +1 -1
  147. package/dist/models/types.js +6 -1
  148. package/dist/models/types.js.map +1 -1
  149. package/dist/pages.d.ts +34 -10
  150. package/dist/pages.d.ts.map +1 -1
  151. package/dist/pages.js +229 -79
  152. package/dist/pages.js.map +1 -1
  153. package/dist/service/createCompletePrompt.d.ts +2 -1
  154. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  155. package/dist/service/createCompletePrompt.js +2 -2
  156. package/dist/service/createCompletePrompt.js.map +1 -1
  157. package/dist/service/requiresSettings.d.ts +2 -1
  158. package/dist/service/requiresSettings.d.ts.map +1 -1
  159. package/dist/service/requiresSettings.js +3 -3
  160. package/dist/service/requiresSettings.js.map +1 -1
  161. package/dist/service/server.d.ts +2 -1
  162. package/dist/service/server.d.ts.map +1 -1
  163. package/dist/service/server.js +37 -8
  164. package/dist/service/server.js.map +1 -1
  165. package/dist/service/transformPage.d.ts +47 -20
  166. package/dist/service/transformPage.d.ts.map +1 -1
  167. package/dist/service/transformPage.js +514 -293
  168. package/dist/service/transformPage.js.map +1 -1
  169. package/dist/service/useAgentRoutes.d.ts +2 -1
  170. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  171. package/dist/service/useAgentRoutes.js +17 -14
  172. package/dist/service/useAgentRoutes.js.map +1 -1
  173. package/dist/service/useApiRoutes.d.ts +2 -1
  174. package/dist/service/useApiRoutes.d.ts.map +1 -1
  175. package/dist/service/useApiRoutes.js +287 -172
  176. package/dist/service/useApiRoutes.js.map +1 -1
  177. package/dist/service/useConnectorRoutes.js +17 -17
  178. package/dist/service/useConnectorRoutes.js.map +1 -1
  179. package/dist/service/useDataRoutes.d.ts.map +1 -1
  180. package/dist/service/useDataRoutes.js +13 -10
  181. package/dist/service/useDataRoutes.js.map +1 -1
  182. package/dist/service/useFileRoutes.d.ts +4 -0
  183. package/dist/service/useFileRoutes.d.ts.map +1 -0
  184. package/dist/service/useFileRoutes.js +122 -0
  185. package/dist/service/useFileRoutes.js.map +1 -0
  186. package/dist/service/usePageRoutes.d.ts +2 -1
  187. package/dist/service/usePageRoutes.d.ts.map +1 -1
  188. package/dist/service/usePageRoutes.js +671 -74
  189. package/dist/service/usePageRoutes.js.map +1 -1
  190. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  191. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  192. package/dist/service/useSharedDataRoutes.js +107 -0
  193. package/dist/service/useSharedDataRoutes.js.map +1 -0
  194. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  195. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  196. package/dist/service/useSharedFileRoutes.js +121 -0
  197. package/dist/service/useSharedFileRoutes.js.map +1 -0
  198. package/dist/settings.d.ts +5 -3
  199. package/dist/settings.d.ts.map +1 -1
  200. package/dist/settings.js +12 -10
  201. package/dist/settings.js.map +1 -1
  202. package/dist/storage/FsStorageProvider.d.ts +25 -0
  203. package/dist/storage/FsStorageProvider.d.ts.map +1 -0
  204. package/dist/storage/FsStorageProvider.js +103 -0
  205. package/dist/storage/FsStorageProvider.js.map +1 -0
  206. package/dist/storage/StorageProvider.d.ts +31 -0
  207. package/dist/storage/StorageProvider.d.ts.map +1 -0
  208. package/dist/storage/StorageProvider.js +3 -0
  209. package/dist/storage/StorageProvider.js.map +1 -0
  210. package/dist/storage/index.d.ts +3 -0
  211. package/dist/storage/index.d.ts.map +1 -0
  212. package/dist/storage/index.js +6 -0
  213. package/dist/storage/index.js.map +1 -0
  214. package/dist/synthos-cli.d.ts.map +1 -1
  215. package/dist/synthos-cli.js +4 -3
  216. package/dist/synthos-cli.js.map +1 -1
  217. package/dist/themes.d.ts +1 -0
  218. package/dist/themes.d.ts.map +1 -1
  219. package/dist/themes.js +65 -28
  220. package/dist/themes.js.map +1 -1
  221. package/migration-rules/v1-to-v2.md +193 -0
  222. package/migration-rules/v2-to-v3.md +481 -0
  223. package/package.json +11 -10
  224. package/required-pages/builder/page.html +43 -0
  225. package/required-pages/builder/page.json +10 -0
  226. package/required-pages/{pages.html → pages/page.html} +238 -233
  227. package/required-pages/pages/page.json +10 -0
  228. package/required-pages/{settings.html → settings/page.html} +389 -275
  229. package/required-pages/settings/page.json +10 -0
  230. package/required-pages/synthos_apis/page.html +846 -0
  231. package/required-pages/synthos_apis/page.json +10 -0
  232. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  233. package/required-pages/synthos_scripts/page.json +10 -0
  234. package/src/agents/index.ts +1 -1
  235. package/src/agents/openclaw/gatewayManager.ts +22 -11
  236. package/src/agents/openclaw/openclawProvider.ts +2 -4
  237. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  238. package/src/builders/anthropic.ts +283 -0
  239. package/src/builders/fireworksai.ts +59 -0
  240. package/src/builders/index.ts +33 -0
  241. package/src/builders/openai.ts +89 -0
  242. package/src/builders/types.ts +261 -0
  243. package/src/connectors/index.ts +1 -1
  244. package/src/connectors/registry.ts +28 -8
  245. package/src/customizer/Customizer.ts +163 -0
  246. package/src/customizer/index.ts +5 -0
  247. package/src/files.ts +57 -0
  248. package/src/index.ts +3 -1
  249. package/src/init.ts +195 -145
  250. package/src/migrations.ts +30 -10
  251. package/src/models/anthropic.ts +40 -10
  252. package/src/models/fireworksai.ts +9 -2
  253. package/src/models/index.ts +1 -1
  254. package/src/models/openai.ts +26 -6
  255. package/src/models/types.ts +31 -1
  256. package/src/pages.ts +230 -77
  257. package/src/service/createCompletePrompt.ts +3 -2
  258. package/src/service/requiresSettings.ts +4 -3
  259. package/src/service/server.ts +36 -9
  260. package/src/service/transformPage.ts +557 -326
  261. package/src/service/useAgentRoutes.ts +19 -14
  262. package/src/service/useApiRoutes.ts +208 -84
  263. package/src/service/useConnectorRoutes.ts +18 -18
  264. package/src/service/useDataRoutes.ts +13 -10
  265. package/src/service/useFileRoutes.ts +128 -0
  266. package/src/service/usePageRoutes.ts +730 -81
  267. package/src/service/useSharedDataRoutes.ts +109 -0
  268. package/src/service/useSharedFileRoutes.ts +127 -0
  269. package/src/settings.ts +14 -10
  270. package/src/storage/FsStorageProvider.ts +87 -0
  271. package/src/storage/StorageProvider.ts +34 -0
  272. package/src/storage/index.ts +2 -0
  273. package/src/synthos-cli.ts +4 -3
  274. package/src/themes.ts +64 -27
  275. package/static-files/favicon.svg +12 -0
  276. package/static-files/fluentlm-instructions.llmd +868 -0
  277. package/static-files/fluentlm-instructions.md +1595 -0
  278. package/static-files/fluentlm.css +4844 -0
  279. package/static-files/fluentlm.js +3602 -0
  280. package/static-files/fluentlm.min.css +1 -0
  281. package/static-files/fluentlm.min.js +1 -0
  282. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  283. package/static-files/page.v3.js +1290 -0
  284. package/static-files/recommended-frameworks.llmd +81 -0
  285. package/static-files/recommended-frameworks.md +137 -0
  286. package/static-files/retro-game.js +877 -0
  287. package/static-files/shell.css +797 -0
  288. package/static-files/theme-dark.css +169 -0
  289. package/static-files/theme-light.css +169 -0
  290. package/tests/builders.spec.ts +139 -0
  291. package/tests/pages.spec.ts +54 -84
  292. package/tests/transformPage.spec.ts +299 -360
  293. package/default-pages/application.html +0 -40
  294. package/default-pages/application.json +0 -1
  295. package/default-pages/json_tools.json +0 -1
  296. package/default-pages/my_notes.html +0 -33
  297. package/default-pages/neon_asteroids.html +0 -77
  298. package/default-pages/sidebar_page.json +0 -1
  299. package/default-pages/solar_tutorial.json +0 -1
  300. package/default-pages/two-panel_page.json +0 -1
  301. package/dist/service/useGatewayRoutes.d.ts +0 -4
  302. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  303. package/dist/service/useGatewayRoutes.js +0 -168
  304. package/dist/service/useGatewayRoutes.js.map +0 -1
  305. package/page-scripts/page-v2.js +0 -656
  306. package/required-pages/builder.html +0 -48
  307. package/required-pages/builder.json +0 -1
  308. package/required-pages/pages.json +0 -1
  309. package/required-pages/settings.json +0 -1
  310. package/required-pages/synthos_apis.html +0 -327
  311. package/required-pages/synthos_apis.json +0 -1
  312. package/required-pages/synthos_scripts.json +0 -1
  313. package/src/connectors/airtable/connector.json +0 -27
  314. package/src/connectors/alpha-vantage/connector.json +0 -26
  315. package/src/connectors/brave-search/connector.json +0 -26
  316. package/src/connectors/cloudinary/connector.json +0 -27
  317. package/src/connectors/deepl/connector.json +0 -28
  318. package/src/connectors/elevenlabs/connector.json +0 -30
  319. package/src/connectors/giphy/connector.json +0 -27
  320. package/src/connectors/github/connector.json +0 -29
  321. package/src/connectors/huggingface/connector.json +0 -27
  322. package/src/connectors/imgur/connector.json +0 -29
  323. package/src/connectors/instagram/connector.json +0 -43
  324. package/src/connectors/jira/connector.json +0 -28
  325. package/src/connectors/mapbox/connector.json +0 -26
  326. package/src/connectors/nasa/connector.json +0 -27
  327. package/src/connectors/newsapi/connector.json +0 -27
  328. package/src/connectors/notion/connector.json +0 -28
  329. package/src/connectors/open-exchange-rates/connector.json +0 -27
  330. package/src/connectors/openweathermap/connector.json +0 -26
  331. package/src/connectors/pexels/connector.json +0 -27
  332. package/src/connectors/resend/connector.json +0 -29
  333. package/src/connectors/rss2json/connector.json +0 -27
  334. package/src/connectors/sendgrid/connector.json +0 -27
  335. package/src/connectors/spoonacular/connector.json +0 -28
  336. package/src/connectors/stability-ai/connector.json +0 -27
  337. package/src/connectors/twilio/connector.json +0 -28
  338. package/src/connectors/unsplash/connector.json +0 -27
  339. package/src/connectors/wolfram-alpha/connector.json +0 -26
  340. package/src/connectors/youtube-data/connector.json +0 -30
  341. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  360. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  361. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  362. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  363. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  364. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  365. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  366. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  367. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  368. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
@@ -0,0 +1,3602 @@
1
+ /**
2
+ * FluentLM — Main Runtime
3
+ *
4
+ * On DOMContentLoaded, initializes the theme and walks the DOM to
5
+ * enhance elements that need JS (icons, split buttons, toggles, etc.).
6
+ *
7
+ * Components that are pure CSS (Stack, Text, Label, Link, Separator,
8
+ * Spinner, TextField, Checkbox, Dropdown, Breadcrumb, Image,
9
+ * ProgressIndicator, Persona, Overlay) require no JS initialization.
10
+ *
11
+ * Load order:
12
+ * 1. icons.js (icon registry — no deps)
13
+ * 2. components/*.js (component modules — depend on FluentIcons)
14
+ * 3. theme.js (theme manager — no deps)
15
+ * 4. fluentlm.js (this file — orchestrator)
16
+ */
17
+ var FluentLM = (function () {
18
+ 'use strict';
19
+
20
+ var initialized = false;
21
+
22
+ /**
23
+ * Initialize all components on the page.
24
+ * Safe to call multiple times (idempotent per element).
25
+ * Optionally pass a root element to scope initialization.
26
+ */
27
+ function init(root) {
28
+ // Theme
29
+ FluentTheme.init();
30
+
31
+ // Tier 1 components
32
+ FluentLMIconComponent.init(root);
33
+ FluentLMButtonComponent.init(root);
34
+ FluentLMToggleComponent.init(root);
35
+ FluentLMMessageBarComponent.init(root);
36
+ FluentLMDropdownComponent.init(root);
37
+
38
+ // Tier 2 components
39
+ FluentLMSearchBoxComponent.init(root);
40
+ FluentLMDialogComponent.init(root);
41
+ FluentLMPanelComponent.init(root);
42
+ FluentLMModalComponent.init(root);
43
+ FluentLMCalloutComponent.init(root);
44
+ FluentLMContextMenuComponent.init(root);
45
+ FluentLMNavComponent.init(root);
46
+ FluentLMPivotComponent.init(root);
47
+ FluentLMTooltipComponent.init(root);
48
+ FluentLMCommandBarComponent.init(root);
49
+
50
+ // Tier 3 components
51
+ FluentLMGroupedListComponent.init(root);
52
+ FluentLMRatingComponent.init(root);
53
+ FluentLMFacepileComponent.init(root);
54
+ FluentLMSwatchColorPickerComponent.init(root);
55
+ FluentLMDocumentCardComponent.init(root);
56
+ FluentLMSpinButtonComponent.init(root);
57
+ FluentLMSliderComponent.init(root);
58
+ FluentLMComboBoxComponent.init(root);
59
+ FluentLMTeachingBubbleComponent.init(root);
60
+ FluentLMHoverCardComponent.init(root);
61
+ FluentLMCoachmarkComponent.init(root);
62
+ FluentLMDatePickerComponent.init(root);
63
+
64
+ // Tier 4 components
65
+ FluentLMTagPickerComponent.init(root);
66
+ FluentLMOverflowSetComponent.init(root);
67
+ FluentLMTimePickerComponent.init(root);
68
+
69
+ initialized = true;
70
+ }
71
+
72
+ /**
73
+ * Re-initialize new elements added after page load.
74
+ * Call this after dynamically inserting HTML with flm-* components.
75
+ * Optionally scope to a container element.
76
+ */
77
+ function refresh(root) {
78
+ FluentLMIconComponent.init(root);
79
+ FluentLMButtonComponent.init(root);
80
+ FluentLMToggleComponent.init(root);
81
+ FluentLMMessageBarComponent.init(root);
82
+ FluentLMDropdownComponent.init(root);
83
+ FluentLMSearchBoxComponent.init(root);
84
+ FluentLMDialogComponent.init(root);
85
+ FluentLMPanelComponent.init(root);
86
+ FluentLMModalComponent.init(root);
87
+ FluentLMCalloutComponent.init(root);
88
+ FluentLMContextMenuComponent.init(root);
89
+ FluentLMNavComponent.init(root);
90
+ FluentLMPivotComponent.init(root);
91
+ FluentLMTooltipComponent.init(root);
92
+ FluentLMCommandBarComponent.init(root);
93
+
94
+ // Tier 3 components
95
+ FluentLMGroupedListComponent.init(root);
96
+ FluentLMRatingComponent.init(root);
97
+ FluentLMFacepileComponent.init(root);
98
+ FluentLMSwatchColorPickerComponent.init(root);
99
+ FluentLMDocumentCardComponent.init(root);
100
+ FluentLMSpinButtonComponent.init(root);
101
+ FluentLMSliderComponent.init(root);
102
+ FluentLMComboBoxComponent.init(root);
103
+ FluentLMTeachingBubbleComponent.init(root);
104
+ FluentLMHoverCardComponent.init(root);
105
+ FluentLMCoachmarkComponent.init(root);
106
+ FluentLMDatePickerComponent.init(root);
107
+
108
+ // Tier 4 components
109
+ FluentLMTagPickerComponent.init(root);
110
+ FluentLMOverflowSetComponent.init(root);
111
+ FluentLMTimePickerComponent.init(root);
112
+ }
113
+
114
+ /**
115
+ * Register a custom theme.
116
+ * @param {string} name - Theme identifier (e.g. 'highcontrast')
117
+ * @param {string} className - CSS class applied to <html> (e.g. 'fluent-highcontrast')
118
+ */
119
+ function registerTheme(name, className) {
120
+ FluentTheme.register(name, className);
121
+ }
122
+
123
+ /**
124
+ * Set theme by name (e.g. 'light', 'dark', or any registered theme).
125
+ */
126
+ function setTheme(theme) {
127
+ FluentTheme.setTheme(theme);
128
+ }
129
+
130
+ /**
131
+ * Toggle to the next theme. Returns new theme name.
132
+ */
133
+ function toggleTheme() {
134
+ return FluentTheme.toggle();
135
+ }
136
+
137
+ /**
138
+ * Get current theme name.
139
+ */
140
+ function getTheme() {
141
+ return FluentTheme.current();
142
+ }
143
+
144
+ // Auto-initialize on DOMContentLoaded
145
+ if (document.readyState === 'loading') {
146
+ document.addEventListener('DOMContentLoaded', function () { init(); });
147
+ } else {
148
+ // DOM already ready (script loaded with defer or at end of body)
149
+ init();
150
+ }
151
+
152
+ // Expose sub-component APIs for direct use
153
+ return {
154
+ init: init,
155
+ refresh: refresh,
156
+ registerTheme: registerTheme,
157
+ setTheme: setTheme,
158
+ toggleTheme: toggleTheme,
159
+ getTheme: getTheme,
160
+ dialog: typeof FluentLMDialogComponent !== 'undefined' ? FluentLMDialogComponent : null,
161
+ panel: typeof FluentLMPanelComponent !== 'undefined' ? FluentLMPanelComponent : null,
162
+ modal: typeof FluentLMModalComponent !== 'undefined' ? FluentLMModalComponent : null,
163
+ callout: typeof FluentLMCalloutComponent !== 'undefined' ? FluentLMCalloutComponent : null,
164
+ contextMenu: typeof FluentLMContextMenuComponent !== 'undefined' ? FluentLMContextMenuComponent : null,
165
+ comboBox: typeof FluentLMComboBoxComponent !== 'undefined' ? FluentLMComboBoxComponent : null,
166
+ datePicker: typeof FluentLMDatePickerComponent !== 'undefined' ? FluentLMDatePickerComponent : null,
167
+ teachingBubble: typeof FluentLMTeachingBubbleComponent !== 'undefined' ? FluentLMTeachingBubbleComponent : null,
168
+ hoverCard: typeof FluentLMHoverCardComponent !== 'undefined' ? FluentLMHoverCardComponent : null,
169
+ tagPicker: typeof FluentLMTagPickerComponent !== 'undefined' ? FluentLMTagPickerComponent : null,
170
+ overflowSet: typeof FluentLMOverflowSetComponent !== 'undefined' ? FluentLMOverflowSetComponent : null,
171
+ timePicker: typeof FluentLMTimePickerComponent !== 'undefined' ? FluentLMTimePickerComponent : null
172
+ };
173
+ })();
174
+
175
+ /**
176
+ * FluentLM — Icon Registry
177
+ *
178
+ * SVG path data for commonly used Fluent UI icons.
179
+ * Each entry is a 16x16 viewBox SVG path string.
180
+ * Add icons as needed; the runtime looks them up by name via data-icon="Name".
181
+ */
182
+ var FluentIcons = (function () {
183
+ 'use strict';
184
+
185
+ // All paths drawn for 16x16 viewBox
186
+ var icons = {
187
+ // Navigation / UI
188
+ ChevronDown: 'M3.15 5.65a.5.5 0 0 1 .7 0L8 9.79l4.15-4.14a.5.5 0 0 1 .7.7l-4.5 4.5a.5.5 0 0 1-.7 0l-4.5-4.5a.5.5 0 0 1 0-.7z',
189
+ ChevronUp: 'M3.15 10.35a.5.5 0 0 1 0-.7L7.29 5.5a.5.5 0 0 1 .71 0l4.15 4.15a.5.5 0 0 1-.71.7L7.65 6.56l-3.8 3.79a.5.5 0 0 1-.7 0z',
190
+ ChevronRight: 'M5.65 3.15a.5.5 0 0 1 .7 0l4.5 4.5a.5.5 0 0 1 0 .7l-4.5 4.5a.5.5 0 0 1-.7-.7L9.79 8 5.65 3.85a.5.5 0 0 1 0-.7z',
191
+ ChevronLeft: 'M10.35 3.15a.5.5 0 0 1 0 .7L6.21 8l4.14 4.15a.5.5 0 0 1-.7.7l-4.5-4.5a.5.5 0 0 1 0-.7l4.5-4.5a.5.5 0 0 1 .7 0z',
192
+ Cancel: 'M2.59 2.59a.5.5 0 0 1 .7 0L8 7.29l4.71-4.7a.5.5 0 0 1 .7.7L8.71 8l4.7 4.71a.5.5 0 0 1-.7.7L8 8.71l-4.71 4.7a.5.5 0 0 1-.7-.7L7.29 8 2.59 3.29a.5.5 0 0 1 0-.7z',
193
+ More: 'M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0zm4.5 0a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0zm4.5 0a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0z',
194
+ Search: 'M6.5 1a5.5 5.5 0 0 1 4.38 8.82l3.15 3.15a.5.5 0 0 1-.7.7l-3.15-3.14A5.5 5.5 0 1 1 6.5 1zm0 1a4.5 4.5 0 1 0 0 9 4.5 4.5 0 0 0 0-9z',
195
+ Filter: 'M1.5 2h13a.5.5 0 0 1 .37.83L10 8.72V13.5a.5.5 0 0 1-.28.45l-3 1.5A.5.5 0 0 1 6 15V8.72L1.13 2.83A.5.5 0 0 1 1.5 2z',
196
+
197
+ // Actions
198
+ Add: 'M8 1.5a.5.5 0 0 1 .5.5v5.5H14a.5.5 0 0 1 0 1H8.5V14a.5.5 0 0 1-1 0V8.5H2a.5.5 0 0 1 0-1h5.5V2a.5.5 0 0 1 .5-.5z',
199
+ Delete: 'M7 3h2a1 1 0 0 0-2 0zM6 3a2 2 0 1 1 4 0h4a.5.5 0 0 1 0 1h-.56l-1.22 9.17A2 2 0 0 1 10.24 15H5.76a2 2 0 0 1-1.98-1.83L2.56 4H2a.5.5 0 0 1 0-1h4zm1 3.5a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5zm3 0a.5.5 0 0 0-1 0v5a.5.5 0 0 0 1 0v-5z',
200
+ Edit: 'M12.92 2.08a2.5 2.5 0 0 0-3.54 0L3.15 8.31a1.5 1.5 0 0 0-.4.65l-.9 3.15a.5.5 0 0 0 .62.62l3.15-.9a1.5 1.5 0 0 0 .65-.4l6.23-6.23a2.5 2.5 0 0 0 0-3.54l-.58-.58z',
201
+ Save: 'M2 3a1 1 0 0 1 1-1h8.59a1.5 1.5 0 0 1 1.06.44l1.91 1.91a1.5 1.5 0 0 1 .44 1.06V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm3 9h6V9H5v3zm7 0v-3.5a.5.5 0 0 0-.5-.5h-7a.5.5 0 0 0-.5.5V12H3V3h1v2.5a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 .5-.5V3h.59l1.91 1.91V12z',
202
+ Copy: 'M4 4.09V10a2 2 0 0 0 2 2h5.91A2 2 0 0 1 10 13H6a3 3 0 0 1-3-3V6a2 2 0 0 1 1-1.91zM6 2h4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2z',
203
+ Undo: 'M3.21 5.5H10a3.5 3.5 0 1 1 0 7H8a.5.5 0 0 1 0-1h2a2.5 2.5 0 0 0 0-5H3.21l2.15 2.15a.5.5 0 0 1-.71.7l-3-3a.5.5 0 0 1 0-.7l3-3a.5.5 0 1 1 .7.7L3.22 5.5z',
204
+ Redo: 'M12.79 5.5H6a3.5 3.5 0 1 0 0 7h2a.5.5 0 0 1 0 1H6a4.5 4.5 0 0 1 0-9h6.79l-2.15-2.15a.5.5 0 0 1 .71-.7l3 3a.5.5 0 0 1 0 .7l-3 3a.5.5 0 0 1-.7-.7L12.78 5.5z',
205
+ Share: 'M12 2.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm0 8a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zM5.5 8a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0z',
206
+
207
+ // Communication
208
+ Mail: 'M2 4.5A1.5 1.5 0 0 1 3.5 3h9A1.5 1.5 0 0 1 14 4.5v7a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 2 11.5v-7zM3.5 4a.5.5 0 0 0-.5.5v.3l5 3 5-3v-.3a.5.5 0 0 0-.5-.5h-9zM13 5.7 8.17 8.56a.5.5 0 0 1-.34 0L3 5.7v5.8a.5.5 0 0 0 .5.5h9a.5.5 0 0 0 .5-.5V5.7z',
209
+ Send: 'M1.72 1.26a.5.5 0 0 1 .53-.05l12.5 6.25a.5.5 0 0 1 0 .9l-12.5 6.24a.5.5 0 0 1-.7-.58L3.27 8.5H7.5a.5.5 0 0 0 0-1H3.27L1.55 1.84a.5.5 0 0 1 .17-.58z',
210
+
211
+ // Status
212
+ Info: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm0 2.5a.75.75 0 1 1 0 1.5.75.75 0 0 1 0-1.5zM8 7a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0v-4A.5.5 0 0 1 8 7z',
213
+ Warning: 'M7.56 1.53a.5.5 0 0 1 .88 0l6.5 12A.5.5 0 0 1 14.5 14h-13a.5.5 0 0 1-.44-.73l6.5-12zM8 5a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3A.5.5 0 0 0 8 5zm0 5.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5z',
214
+ ErrorBadge: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm2.85 3.15a.5.5 0 0 1 0 .7L8.71 8l2.14 2.15a.5.5 0 0 1-.7.7L8 8.71l-2.15 2.14a.5.5 0 0 1-.7-.7L7.29 8 5.15 5.85a.5.5 0 0 1 .7-.7L8 7.29l2.15-2.14a.5.5 0 0 1 .7 0z',
215
+ Completed: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm3.35 3.65a.5.5 0 0 1 0 .7l-4 4a.5.5 0 0 1-.7 0l-2-2a.5.5 0 0 1 .7-.7L7 9.29l3.65-3.64a.5.5 0 0 1 .7 0z',
216
+ Blocked: 'M8 1a7 7 0 1 1 0 14A7 7 0 0 1 8 1zm0 1a6 6 0 1 0 0 12A6 6 0 0 0 8 2zm3.46 2.54a.5.5 0 0 1 0 .7l-6.22 6.22a.5.5 0 0 1-.7-.7l6.22-6.22a.5.5 0 0 1 .7 0z',
217
+ Checkmark: 'M13.86 3.66a.5.5 0 0 1-.02.7l-7.93 7.48a.5.5 0 0 1-.7-.02L2.16 8.59a.5.5 0 0 1 .72-.7l2.7 2.88 7.58-7.13a.5.5 0 0 1 .7.02z',
218
+
219
+ // Objects
220
+ Settings: 'M7.2 1a.8.8 0 0 0-.79.65l-.28 1.5a5.53 5.53 0 0 0-1.18.68l-1.42-.57a.8.8 0 0 0-.97.33l-.8 1.38a.8.8 0 0 0 .18.99l1.14.94a5.6 5.6 0 0 0 0 1.36l-1.14.94a.8.8 0 0 0-.18.98l.8 1.38a.8.8 0 0 0 .97.34l1.42-.57c.36.27.76.5 1.18.68l.28 1.5a.8.8 0 0 0 .79.66h1.6a.8.8 0 0 0 .79-.65l.28-1.5a5.5 5.5 0 0 0 1.18-.69l1.42.57a.8.8 0 0 0 .97-.33l.8-1.38a.8.8 0 0 0-.18-.99l-1.14-.94a5.5 5.5 0 0 0 0-1.36l1.14-.94a.8.8 0 0 0 .18-.98l-.8-1.38a.8.8 0 0 0-.97-.34l-1.42.57a5.5 5.5 0 0 0-1.18-.68l-.28-1.5A.8.8 0 0 0 8.8 1H7.2zM8 5.5a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5z',
221
+ Home: 'M7.65 1.15a.5.5 0 0 1 .7 0l6 6a.5.5 0 0 1-.7.7l-.65-.64V13a1 1 0 0 1-1 1h-2.5a.5.5 0 0 1-.5-.5V10H7v3.5a.5.5 0 0 1-.5.5H4a1 1 0 0 1-1-1V7.21l-.65.64a.5.5 0 0 1-.7-.7l6-6z',
222
+ Calendar: 'M4.5 1a.5.5 0 0 1 .5.5V3h6V1.5a.5.5 0 0 1 1 0V3h1.5A1.5 1.5 0 0 1 15 4.5v8a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-8A1.5 1.5 0 0 1 2.5 3H4V1.5a.5.5 0 0 1 .5-.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z',
223
+ Person: 'M8 1a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm0 8c3.5 0 6 1.75 6 3.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-.5C2 10.75 4.5 9 8 9z',
224
+ Document: 'M4 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V5.41a1 1 0 0 0-.3-.7L9.3 1.28a1 1 0 0 0-.71-.29H4zm5 0v3.5a.5.5 0 0 0 .5.5H13',
225
+ Attach: 'M7.73 1.82a3.01 3.01 0 0 1 4.24 0 3.01 3.01 0 0 1 0 4.24l-5.3 5.3a1.75 1.75 0 0 1-2.47-2.47l5.3-5.3a.5.5 0 0 1 .7.7l-5.3 5.3a.75.75 0 1 0 1.07 1.07l5.3-5.3a2.01 2.01 0 0 0-2.84-2.84l-5.3 5.3a3.25 3.25 0 1 0 4.6 4.6l5.3-5.3a.5.5 0 0 1 .7.7l-5.3 5.3a4.25 4.25 0 0 1-6-6l5.3-5.3z',
226
+ };
227
+
228
+ /**
229
+ * Return an SVG element for the given icon name.
230
+ * Returns null if the name is not registered.
231
+ */
232
+ function getSvg(name) {
233
+ var path = icons[name];
234
+ if (!path) return null;
235
+
236
+ var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
237
+ svg.setAttribute('viewBox', '0 0 16 16');
238
+ svg.setAttribute('aria-hidden', 'true');
239
+ svg.setAttribute('focusable', 'false');
240
+ svg.style.width = '1em';
241
+ svg.style.height = '1em';
242
+ svg.style.fill = 'currentColor';
243
+
244
+ var p = document.createElementNS('http://www.w3.org/2000/svg', 'path');
245
+ p.setAttribute('d', path);
246
+ svg.appendChild(p);
247
+ return svg;
248
+ }
249
+
250
+ /**
251
+ * Register a custom icon (or override a built-in one).
252
+ */
253
+ function register(name, pathData) {
254
+ icons[name] = pathData;
255
+ }
256
+
257
+ return {
258
+ getSvg: getSvg,
259
+ register: register
260
+ };
261
+ })();
262
+
263
+ /**
264
+ * FluentLM — Theme Manager
265
+ *
266
+ * Handles theme switching via class on <html>.
267
+ * Respects prefers-color-scheme on first load.
268
+ *
269
+ * Built-in themes: light, dark.
270
+ * Register custom themes with FluentTheme.register(name, className).
271
+ */
272
+ var FluentTheme = (function () {
273
+ 'use strict';
274
+
275
+ var TRANSITION = 'fluent-theme-transition';
276
+ var STORAGE_KEY = 'fluentlm-theme';
277
+
278
+ // Theme registry: name → CSS class
279
+ var themes = {
280
+ light: 'fluentlm',
281
+ dark: 'fluent-dark'
282
+ };
283
+
284
+ // Ordered list of theme names for cycling
285
+ var themeOrder = ['light', 'dark'];
286
+
287
+ /**
288
+ * Register a custom theme.
289
+ * @param {string} name - Theme identifier (used with setTheme/getTheme)
290
+ * @param {string} className - CSS class applied to <html>
291
+ */
292
+ function register(name, className) {
293
+ if (!name || !className) { return; }
294
+ themes[name] = className;
295
+ if (themeOrder.indexOf(name) === -1) {
296
+ themeOrder.push(name);
297
+ }
298
+ }
299
+
300
+ function allClasses() {
301
+ var classes = [];
302
+ for (var key in themes) {
303
+ if (themes.hasOwnProperty(key)) { classes.push(themes[key]); }
304
+ }
305
+ return classes;
306
+ }
307
+
308
+ function current() {
309
+ var html = document.documentElement;
310
+ for (var i = themeOrder.length - 1; i >= 0; i--) {
311
+ if (html.classList.contains(themes[themeOrder[i]])) {
312
+ return themeOrder[i];
313
+ }
314
+ }
315
+ return 'light';
316
+ }
317
+
318
+ function setTheme(theme) {
319
+ var className = themes[theme];
320
+ if (!className) { return; }
321
+
322
+ var html = document.documentElement;
323
+ html.classList.add(TRANSITION);
324
+
325
+ var classes = allClasses();
326
+ for (var i = 0; i < classes.length; i++) {
327
+ html.classList.remove(classes[i]);
328
+ }
329
+ html.classList.add(className);
330
+
331
+ try { localStorage.setItem(STORAGE_KEY, theme); } catch (e) { /* noop */ }
332
+
333
+ setTimeout(function () {
334
+ html.classList.remove(TRANSITION);
335
+ }, 250);
336
+ }
337
+
338
+ function toggle() {
339
+ var idx = themeOrder.indexOf(current());
340
+ var next = themeOrder[(idx + 1) % themeOrder.length];
341
+ setTheme(next);
342
+ return current();
343
+ }
344
+
345
+ function init() {
346
+ var html = document.documentElement;
347
+
348
+ // 1. Check localStorage
349
+ var stored;
350
+ try { stored = localStorage.getItem(STORAGE_KEY); } catch (e) { /* noop */ }
351
+
352
+ if (stored && themes[stored]) {
353
+ var classes = allClasses();
354
+ for (var i = 0; i < classes.length; i++) {
355
+ html.classList.remove(classes[i]);
356
+ }
357
+ html.classList.add(themes[stored]);
358
+ return;
359
+ }
360
+
361
+ // 2. Check OS preference (only if no theme class is already set)
362
+ var classes = allClasses();
363
+ var hasTheme = false;
364
+ for (var i = 0; i < classes.length; i++) {
365
+ if (html.classList.contains(classes[i])) { hasTheme = true; break; }
366
+ }
367
+ if (!hasTheme) {
368
+ var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
369
+ html.classList.add(prefersDark ? themes.dark : themes.light);
370
+ }
371
+ }
372
+
373
+ return {
374
+ init: init,
375
+ current: current,
376
+ setTheme: setTheme,
377
+ toggle: toggle,
378
+ register: register
379
+ };
380
+ })();
381
+
382
+ /**
383
+ * Button component JS — handles split buttons and icon injection.
384
+ * Icon injection is delegated to FluentLMIconComponent.
385
+ * This module handles data-split transformation.
386
+ */
387
+ var FluentLMButtonComponent = (function () {
388
+ 'use strict';
389
+
390
+ function init(root) {
391
+ var els = (root || document).querySelectorAll('.flm-button[data-split]');
392
+ for (var i = 0; i < els.length; i++) {
393
+ renderSplit(els[i]);
394
+ }
395
+ }
396
+
397
+ function renderSplit(btn) {
398
+ // Skip if already rendered
399
+ if (btn.getAttribute('data-split-rendered')) return;
400
+
401
+ // Wrap button in a split container
402
+ var wrapper = document.createElement('div');
403
+ wrapper.className = 'flm-button-split';
404
+ if (btn.classList.contains('flm-button--primary')) {
405
+ wrapper.classList.add('flm-button-split--primary');
406
+ }
407
+
408
+ // Create caret button
409
+ var caret = document.createElement('button');
410
+ caret.className = 'flm-button-split-caret';
411
+ caret.setAttribute('aria-label', 'See more options');
412
+ caret.setAttribute('aria-haspopup', 'true');
413
+ caret.type = 'button';
414
+
415
+ var chevron = FluentIcons.getSvg('ChevronDown');
416
+ if (chevron) {
417
+ caret.appendChild(chevron);
418
+ }
419
+
420
+ // Insert wrapper and move button into it
421
+ btn.parentNode.insertBefore(wrapper, btn);
422
+ btn.removeAttribute('data-split');
423
+ wrapper.appendChild(btn);
424
+ wrapper.appendChild(caret);
425
+
426
+ btn.setAttribute('data-split-rendered', 'true');
427
+ }
428
+
429
+ return { init: init };
430
+ })();
431
+
432
+ /**
433
+ * Callout component JS — positions a callout relative to a target element.
434
+ *
435
+ * Usage:
436
+ * FluentLMCalloutComponent.show(calloutEl, targetEl)
437
+ * FluentLMCalloutComponent.hide(calloutEl)
438
+ *
439
+ * Or declarative: <button data-callout-toggle="my-callout">Toggle</button>
440
+ */
441
+ var FluentLMCalloutComponent = (function () {
442
+ 'use strict';
443
+
444
+ function init(root) {
445
+ var doc = root || document;
446
+
447
+ var triggers = doc.querySelectorAll('[data-callout-toggle]');
448
+ for (var i = 0; i < triggers.length; i++) {
449
+ wireTrigger(triggers[i]);
450
+ }
451
+ }
452
+
453
+ function wireTrigger(btn) {
454
+ if (btn.getAttribute('data-callout-wired')) return;
455
+ btn.addEventListener('click', function (e) {
456
+ var id = btn.getAttribute('data-callout-toggle');
457
+ var callout = document.getElementById(id);
458
+ if (!callout) return;
459
+
460
+ if (callout.classList.contains('flm-callout--visible')) {
461
+ hide(callout);
462
+ } else {
463
+ show(callout, btn);
464
+ }
465
+ e.stopPropagation();
466
+ });
467
+ btn.setAttribute('data-callout-wired', 'true');
468
+ }
469
+
470
+ function show(callout, target) {
471
+ // Position relative to target
472
+ var rect = target.getBoundingClientRect();
473
+ var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
474
+ var scrollY = window.pageYOffset || document.documentElement.scrollTop;
475
+
476
+ callout.style.position = 'absolute';
477
+ callout.style.left = rect.left + scrollX + 'px';
478
+ callout.style.top = (rect.bottom + scrollY + 4) + 'px';
479
+
480
+ callout.classList.add('flm-callout--visible');
481
+ callout.classList.add('flm-callout--below');
482
+
483
+ // Check if callout goes off-screen bottom, flip above if needed
484
+ setTimeout(function () {
485
+ var calloutRect = callout.getBoundingClientRect();
486
+ if (calloutRect.bottom > window.innerHeight) {
487
+ callout.classList.remove('flm-callout--below');
488
+ callout.classList.add('flm-callout--above');
489
+ callout.style.top = (rect.top + scrollY - calloutRect.height - 4) + 'px';
490
+ }
491
+ }, 0);
492
+
493
+ // Click outside to dismiss
494
+ var outsideHandler = function (e) {
495
+ if (!callout.contains(e.target) && !target.contains(e.target)) {
496
+ hide(callout);
497
+ document.removeEventListener('click', outsideHandler);
498
+ }
499
+ };
500
+ setTimeout(function () {
501
+ document.addEventListener('click', outsideHandler);
502
+ }, 0);
503
+ callout._outsideHandler = outsideHandler;
504
+ }
505
+
506
+ function hide(callout) {
507
+ callout.classList.remove('flm-callout--visible', 'flm-callout--below', 'flm-callout--above');
508
+ if (callout._outsideHandler) {
509
+ document.removeEventListener('click', callout._outsideHandler);
510
+ delete callout._outsideHandler;
511
+ }
512
+ }
513
+
514
+ return { init: init, show: show, hide: hide };
515
+ })();
516
+
517
+ /**
518
+ * Coachmark component JS — pulsing beacon that opens a TeachingBubble on click.
519
+ *
520
+ * Usage:
521
+ * <div class="flm-coachmark" data-teachingbubble-toggle="tb1">
522
+ * <div class="flm-coachmark-dot"></div>
523
+ * <div class="flm-coachmark-ring"></div>
524
+ * </div>
525
+ * <div class="flm-teachingbubble" id="tb1">...</div>
526
+ *
527
+ * Uses MutationObserver to auto-hide beacon when the teaching bubble is dismissed.
528
+ */
529
+ var FluentLMCoachmarkComponent = (function () {
530
+ 'use strict';
531
+
532
+ function init(root) {
533
+ var doc = root || document;
534
+
535
+ var coachmarks = doc.querySelectorAll('.flm-coachmark');
536
+ for (var i = 0; i < coachmarks.length; i++) {
537
+ wireCoachmark(coachmarks[i]);
538
+ }
539
+ }
540
+
541
+ function wireCoachmark(el) {
542
+ if (el.getAttribute('data-coachmark-wired')) return;
543
+
544
+ var bubbleId = el.getAttribute('data-teachingbubble-toggle');
545
+ if (!bubbleId) return;
546
+
547
+ // Click handler to open teaching bubble
548
+ el.addEventListener('click', function () {
549
+ var bubble = document.getElementById(bubbleId);
550
+ if (!bubble) return;
551
+
552
+ if (typeof FluentLMTeachingBubbleComponent !== 'undefined') {
553
+ FluentLMTeachingBubbleComponent.show(bubble, el);
554
+ }
555
+ });
556
+
557
+ // Observe the teaching bubble for dismiss to hide beacon
558
+ var bubble = document.getElementById(bubbleId);
559
+ if (bubble && typeof MutationObserver !== 'undefined') {
560
+ var observer = new MutationObserver(function (mutations) {
561
+ for (var i = 0; i < mutations.length; i++) {
562
+ if (mutations[i].attributeName === 'class') {
563
+ if (!bubble.classList.contains('flm-teachingbubble--visible')) {
564
+ el.classList.add('flm-coachmark--hidden');
565
+ observer.disconnect();
566
+ }
567
+ }
568
+ }
569
+ });
570
+
571
+ observer.observe(bubble, { attributes: true, attributeFilter: ['class'] });
572
+ }
573
+
574
+ el.setAttribute('data-coachmark-wired', 'true');
575
+ }
576
+
577
+ return { init: init };
578
+ })();
579
+
580
+ /**
581
+ * ComboBox component JS — filterable dropdown with keyboard navigation.
582
+ *
583
+ * Usage:
584
+ * <div class="flm-combobox">
585
+ * <div class="flm-combobox-wrapper">
586
+ * <input class="flm-combobox-input" placeholder="Select...">
587
+ * <button class="flm-combobox-caret" data-icon="ChevronDown" aria-label="Toggle"></button>
588
+ * </div>
589
+ * <div class="flm-combobox-listbox">
590
+ * <div class="flm-combobox-option" data-value="a">Alpha</div>
591
+ * <div class="flm-combobox-option" data-value="b">Beta</div>
592
+ * </div>
593
+ * </div>
594
+ *
595
+ * Add data-multiselect on .flm-combobox for multi-select mode.
596
+ */
597
+ var FluentLMComboBoxComponent = (function () {
598
+ 'use strict';
599
+
600
+ function init(root) {
601
+ var doc = root || document;
602
+
603
+ var combos = doc.querySelectorAll('.flm-combobox');
604
+ for (var i = 0; i < combos.length; i++) {
605
+ wireCombo(combos[i]);
606
+ }
607
+ }
608
+
609
+ function wireCombo(el) {
610
+ if (el.getAttribute('data-combobox-wired')) return;
611
+
612
+ var input = el.querySelector('.flm-combobox-input');
613
+ var caret = el.querySelector('.flm-combobox-caret');
614
+ var listbox = el.querySelector('.flm-combobox-listbox');
615
+
616
+ if (!input || !listbox) return;
617
+
618
+ var multiselect = el.hasAttribute('data-multiselect');
619
+ var highlighted = -1;
620
+
621
+ function getOptions() {
622
+ return listbox.querySelectorAll('.flm-combobox-option:not(.flm-combobox-option--disabled):not(.flm-combobox-option--hidden)');
623
+ }
624
+
625
+ function isOpen() {
626
+ return listbox.classList.contains('flm-combobox-listbox--open');
627
+ }
628
+
629
+ function open() {
630
+ document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
631
+ listbox.classList.add('flm-combobox-listbox--open');
632
+ highlighted = -1;
633
+ flipIfNeeded();
634
+ }
635
+
636
+ function close() {
637
+ listbox.classList.remove('flm-combobox-listbox--open', 'flm-combobox-listbox--above');
638
+ highlighted = -1;
639
+ clearHighlight();
640
+ }
641
+
642
+ function flipIfNeeded() {
643
+ setTimeout(function () {
644
+ var rect = listbox.getBoundingClientRect();
645
+ if (rect.bottom > window.innerHeight) {
646
+ listbox.classList.add('flm-combobox-listbox--above');
647
+ } else {
648
+ listbox.classList.remove('flm-combobox-listbox--above');
649
+ }
650
+ }, 0);
651
+ }
652
+
653
+ function clearHighlight() {
654
+ var opts = listbox.querySelectorAll('.flm-combobox-option--highlighted');
655
+ for (var i = 0; i < opts.length; i++) {
656
+ opts[i].classList.remove('flm-combobox-option--highlighted');
657
+ }
658
+ }
659
+
660
+ function setHighlight(idx) {
661
+ clearHighlight();
662
+ var opts = getOptions();
663
+ if (idx >= 0 && idx < opts.length) {
664
+ highlighted = idx;
665
+ opts[idx].classList.add('flm-combobox-option--highlighted');
666
+ opts[idx].scrollIntoView({ block: 'nearest' });
667
+ }
668
+ }
669
+
670
+ function filterOptions() {
671
+ var text = input.value.toLowerCase();
672
+ var allOpts = listbox.querySelectorAll('.flm-combobox-option');
673
+ for (var i = 0; i < allOpts.length; i++) {
674
+ var optText = allOpts[i].textContent.toLowerCase();
675
+ if (text === '' || optText.indexOf(text) !== -1) {
676
+ allOpts[i].classList.remove('flm-combobox-option--hidden');
677
+ } else {
678
+ allOpts[i].classList.add('flm-combobox-option--hidden');
679
+ }
680
+ }
681
+ highlighted = -1;
682
+ }
683
+
684
+ function selectOption(opt) {
685
+ var value = opt.getAttribute('data-value') || opt.textContent;
686
+ var text = opt.textContent;
687
+
688
+ if (multiselect) {
689
+ opt.classList.toggle('flm-combobox-option--selected');
690
+ // Build comma-separated value
691
+ var selected = listbox.querySelectorAll('.flm-combobox-option--selected');
692
+ var values = [];
693
+ for (var i = 0; i < selected.length; i++) {
694
+ values.push(selected[i].textContent);
695
+ }
696
+ input.value = values.join(', ');
697
+ } else {
698
+ // Clear previous selection
699
+ var prev = listbox.querySelectorAll('.flm-combobox-option--selected');
700
+ for (var j = 0; j < prev.length; j++) {
701
+ prev[j].classList.remove('flm-combobox-option--selected');
702
+ }
703
+ opt.classList.add('flm-combobox-option--selected');
704
+ input.value = text;
705
+ el.setAttribute('data-value', value);
706
+ close();
707
+ }
708
+
709
+ // Fire change event
710
+ var evt = document.createEvent('Event');
711
+ evt.initEvent('change', true, true);
712
+ input.dispatchEvent(evt);
713
+ }
714
+
715
+ // Input events
716
+ input.addEventListener('focus', function () {
717
+ if (!isOpen()) open();
718
+ });
719
+
720
+ input.addEventListener('input', function () {
721
+ if (!isOpen()) open();
722
+ filterOptions();
723
+ });
724
+
725
+ // Caret toggle
726
+ if (caret) {
727
+ caret.addEventListener('click', function (e) {
728
+ e.stopPropagation();
729
+ if (isOpen()) {
730
+ close();
731
+ } else {
732
+ filterOptions();
733
+ open();
734
+ input.focus();
735
+ }
736
+ });
737
+ }
738
+
739
+ // Keyboard navigation
740
+ input.addEventListener('keydown', function (e) {
741
+ var opts = getOptions();
742
+ var len = opts.length;
743
+
744
+ if (e.key === 'ArrowDown' || e.keyCode === 40) {
745
+ e.preventDefault();
746
+ if (!isOpen()) { open(); filterOptions(); }
747
+ setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
748
+ } else if (e.key === 'ArrowUp' || e.keyCode === 38) {
749
+ e.preventDefault();
750
+ if (!isOpen()) { open(); filterOptions(); }
751
+ setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
752
+ } else if (e.key === 'Enter' || e.keyCode === 13) {
753
+ e.preventDefault();
754
+ if (highlighted >= 0 && highlighted < len) {
755
+ selectOption(opts[highlighted]);
756
+ }
757
+ } else if (e.key === 'Escape' || e.keyCode === 27) {
758
+ close();
759
+ }
760
+ });
761
+
762
+ // Option click
763
+ listbox.addEventListener('click', function (e) {
764
+ var opt = e.target.closest('.flm-combobox-option');
765
+ if (opt && !opt.classList.contains('flm-combobox-option--disabled')) {
766
+ selectOption(opt);
767
+ }
768
+ });
769
+
770
+ // Click outside
771
+ document.addEventListener('click', function (e) {
772
+ if (!el.contains(e.target) && isOpen()) {
773
+ close();
774
+ }
775
+ });
776
+
777
+ // Close when another picker opens
778
+ document.addEventListener('flm-dismiss-pickers', function (e) {
779
+ if (e.detail.source !== el && isOpen()) close();
780
+ });
781
+
782
+ el.setAttribute('data-combobox-wired', 'true');
783
+ }
784
+
785
+ return { init: init };
786
+ })();
787
+
788
+ /**
789
+ * CommandBar component JS — injects icons for command bar items with data-icon.
790
+ */
791
+ var FluentLMCommandBarComponent = (function () {
792
+ 'use strict';
793
+
794
+ function init(root) {
795
+ var doc = root || document;
796
+
797
+ // Inject icons into commandbar items that have data-icon
798
+ var items = doc.querySelectorAll('.flm-commandbar-item[data-icon]');
799
+ for (var i = 0; i < items.length; i++) {
800
+ FluentLMIconComponent.render(items[i]);
801
+ }
802
+
803
+ // Inject overflow icon
804
+ var overflows = doc.querySelectorAll('.flm-commandbar-overflow');
805
+ for (var j = 0; j < overflows.length; j++) {
806
+ if (!overflows[j].getAttribute('data-icon-rendered')) {
807
+ var svg = FluentIcons.getSvg('More');
808
+ if (svg) {
809
+ overflows[j].innerHTML = '';
810
+ overflows[j].appendChild(svg);
811
+ overflows[j].setAttribute('data-icon-rendered', 'true');
812
+ }
813
+ }
814
+ }
815
+ }
816
+
817
+ return { init: init };
818
+ })();
819
+
820
+ /**
821
+ * ContextualMenu component JS — show/hide, positioning, click-outside dismiss.
822
+ *
823
+ * Usage:
824
+ * FluentLMContextMenuComponent.show(menuEl, targetEl)
825
+ * FluentLMContextMenuComponent.hide(menuEl)
826
+ *
827
+ * Or: <button data-contextmenu-toggle="my-menu">Menu</button>
828
+ * Or: Right-click trigger: <div data-contextmenu="my-menu">Right-click me</div>
829
+ */
830
+ var FluentLMContextMenuComponent = (function () {
831
+ 'use strict';
832
+
833
+ function init(root) {
834
+ var doc = root || document;
835
+
836
+ // Button triggers
837
+ var triggers = doc.querySelectorAll('[data-contextmenu-toggle]');
838
+ for (var i = 0; i < triggers.length; i++) {
839
+ wireTrigger(triggers[i]);
840
+ }
841
+
842
+ // Right-click triggers
843
+ var contextTriggers = doc.querySelectorAll('[data-contextmenu]');
844
+ for (var j = 0; j < contextTriggers.length; j++) {
845
+ wireContextTrigger(contextTriggers[j]);
846
+ }
847
+
848
+ // Wire menu item clicks for dismiss
849
+ var menus = doc.querySelectorAll('.flm-contextmenu');
850
+ for (var k = 0; k < menus.length; k++) {
851
+ wireMenuItems(menus[k]);
852
+ }
853
+ }
854
+
855
+ function wireTrigger(btn) {
856
+ if (btn.getAttribute('data-cm-wired')) return;
857
+ btn.addEventListener('click', function (e) {
858
+ var id = btn.getAttribute('data-contextmenu-toggle');
859
+ var menu = document.getElementById(id);
860
+ if (!menu) return;
861
+ if (menu.classList.contains('flm-contextmenu--visible')) {
862
+ hide(menu);
863
+ } else {
864
+ show(menu, btn);
865
+ }
866
+ e.stopPropagation();
867
+ });
868
+ btn.setAttribute('data-cm-wired', 'true');
869
+ }
870
+
871
+ function wireContextTrigger(el) {
872
+ if (el.getAttribute('data-cm-wired')) return;
873
+ el.addEventListener('contextmenu', function (e) {
874
+ e.preventDefault();
875
+ var id = el.getAttribute('data-contextmenu');
876
+ var menu = document.getElementById(id);
877
+ if (!menu) return;
878
+ showAt(menu, e.pageX, e.pageY);
879
+ });
880
+ el.setAttribute('data-cm-wired', 'true');
881
+ }
882
+
883
+ function wireMenuItems(menu) {
884
+ var items = menu.querySelectorAll('.flm-contextmenu-item');
885
+ for (var i = 0; i < items.length; i++) {
886
+ items[i].addEventListener('click', function () {
887
+ hide(menu);
888
+ });
889
+ }
890
+ }
891
+
892
+ function show(menu, target) {
893
+ var rect = target.getBoundingClientRect();
894
+ var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
895
+ var scrollY = window.pageYOffset || document.documentElement.scrollTop;
896
+ showAt(menu, rect.left + scrollX, rect.bottom + scrollY + 2);
897
+ }
898
+
899
+ function showAt(menu, x, y) {
900
+ menu.style.position = 'absolute';
901
+ menu.style.left = x + 'px';
902
+ menu.style.top = y + 'px';
903
+ menu.classList.add('flm-contextmenu--visible');
904
+
905
+ // Reposition if off-screen
906
+ setTimeout(function () {
907
+ var menuRect = menu.getBoundingClientRect();
908
+ if (menuRect.right > window.innerWidth) {
909
+ menu.style.left = (x - menuRect.width) + 'px';
910
+ }
911
+ if (menuRect.bottom > window.innerHeight) {
912
+ menu.style.top = (y - menuRect.height) + 'px';
913
+ }
914
+ }, 0);
915
+
916
+ var outsideHandler = function (e) {
917
+ if (!menu.contains(e.target)) {
918
+ hide(menu);
919
+ document.removeEventListener('click', outsideHandler);
920
+ }
921
+ };
922
+ setTimeout(function () {
923
+ document.addEventListener('click', outsideHandler);
924
+ }, 0);
925
+ menu._outsideHandler = outsideHandler;
926
+ }
927
+
928
+ function hide(menu) {
929
+ menu.classList.remove('flm-contextmenu--visible');
930
+ if (menu._outsideHandler) {
931
+ document.removeEventListener('click', menu._outsideHandler);
932
+ delete menu._outsideHandler;
933
+ }
934
+ }
935
+
936
+ return { init: init, show: show, showAt: showAt, hide: hide };
937
+ })();
938
+
939
+ /**
940
+ * DatePicker component JS — generates calendar grid and handles date selection.
941
+ *
942
+ * Usage:
943
+ * <div class="flm-datepicker">
944
+ * <label class="flm-label" for="dp1">Date</label>
945
+ * <div class="flm-datepicker-wrapper">
946
+ * <input class="flm-datepicker-input" id="dp1" placeholder="MM/DD/YYYY">
947
+ * <button class="flm-datepicker-icon" data-icon="Calendar" aria-label="Open calendar"></button>
948
+ * </div>
949
+ * </div>
950
+ *
951
+ * Attributes on .flm-datepicker:
952
+ * data-min-date="MM/DD/YYYY"
953
+ * data-max-date="MM/DD/YYYY"
954
+ */
955
+ var FluentLMDatePickerComponent = (function () {
956
+ 'use strict';
957
+
958
+ var DAYS = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'];
959
+ var MONTHS = [
960
+ 'January', 'February', 'March', 'April', 'May', 'June',
961
+ 'July', 'August', 'September', 'October', 'November', 'December'
962
+ ];
963
+
964
+ function init(root) {
965
+ var doc = root || document;
966
+
967
+ var pickers = doc.querySelectorAll('.flm-datepicker');
968
+ for (var i = 0; i < pickers.length; i++) {
969
+ wirePicker(pickers[i]);
970
+ }
971
+ }
972
+
973
+ function wirePicker(el) {
974
+ if (el.getAttribute('data-datepicker-wired')) return;
975
+
976
+ var input = el.querySelector('.flm-datepicker-input');
977
+ var iconBtn = el.querySelector('.flm-datepicker-icon');
978
+ if (!input) return;
979
+
980
+ var callout = null;
981
+ var viewYear, viewMonth, selectedDate;
982
+
983
+ // Parse min/max dates
984
+ var minDate = parseDate(el.getAttribute('data-min-date'));
985
+ var maxDate = parseDate(el.getAttribute('data-max-date'));
986
+
987
+ function parseDate(str) {
988
+ if (!str) return null;
989
+ var parts = str.split('/');
990
+ if (parts.length !== 3) return null;
991
+ return new Date(parseInt(parts[2], 10), parseInt(parts[0], 10) - 1, parseInt(parts[1], 10));
992
+ }
993
+
994
+ function formatDate(d) {
995
+ var mm = (d.getMonth() + 1);
996
+ var dd = d.getDate();
997
+ var yyyy = d.getFullYear();
998
+ return (mm < 10 ? '0' : '') + mm + '/' + (dd < 10 ? '0' : '') + dd + '/' + yyyy;
999
+ }
1000
+
1001
+ function ensureCallout() {
1002
+ if (callout) return callout;
1003
+
1004
+ callout = document.createElement('div');
1005
+ callout.className = 'flm-datepicker-callout';
1006
+ el.appendChild(callout);
1007
+ return callout;
1008
+ }
1009
+
1010
+ function isOpen() {
1011
+ return callout && callout.classList.contains('flm-datepicker-callout--open');
1012
+ }
1013
+
1014
+ function open() {
1015
+ document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
1016
+ ensureCallout();
1017
+ var today = new Date();
1018
+ viewYear = selectedDate ? selectedDate.getFullYear() : today.getFullYear();
1019
+ viewMonth = selectedDate ? selectedDate.getMonth() : today.getMonth();
1020
+ renderCalendar();
1021
+ callout.classList.add('flm-datepicker-callout--open');
1022
+ flipIfNeeded();
1023
+ }
1024
+
1025
+ function close() {
1026
+ if (callout) {
1027
+ callout.classList.remove('flm-datepicker-callout--open', 'flm-datepicker-callout--above');
1028
+ }
1029
+ }
1030
+
1031
+ function flipIfNeeded() {
1032
+ setTimeout(function () {
1033
+ if (!callout) return;
1034
+ var rect = callout.getBoundingClientRect();
1035
+ if (rect.bottom > window.innerHeight) {
1036
+ callout.classList.add('flm-datepicker-callout--above');
1037
+ } else {
1038
+ callout.classList.remove('flm-datepicker-callout--above');
1039
+ }
1040
+ }, 0);
1041
+ }
1042
+
1043
+ function renderCalendar() {
1044
+ var html = '';
1045
+
1046
+ // Navigation
1047
+ html += '<div class="flm-datepicker-nav">';
1048
+ html += '<button class="flm-datepicker-nav-btn flm-datepicker-prev" data-icon="ChevronLeft" aria-label="Previous month"></button>';
1049
+ html += '<span class="flm-datepicker-month">' + MONTHS[viewMonth] + ' ' + viewYear + '</span>';
1050
+ html += '<button class="flm-datepicker-nav-btn flm-datepicker-next" data-icon="ChevronRight" aria-label="Next month"></button>';
1051
+ html += '</div>';
1052
+
1053
+ // Weekday headers
1054
+ html += '<div class="flm-datepicker-weekdays">';
1055
+ for (var d = 0; d < 7; d++) {
1056
+ html += '<span class="flm-datepicker-weekday">' + DAYS[d] + '</span>';
1057
+ }
1058
+ html += '</div>';
1059
+
1060
+ // Calendar grid (42 cells)
1061
+ html += '<div class="flm-datepicker-grid">';
1062
+
1063
+ var firstDay = new Date(viewYear, viewMonth, 1).getDay();
1064
+ var daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
1065
+ var daysInPrev = new Date(viewYear, viewMonth, 0).getDate();
1066
+ var today = new Date();
1067
+ today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
1068
+
1069
+ var cellDate;
1070
+ for (var i = 0; i < 42; i++) {
1071
+ var dayNum;
1072
+ var isOutside = false;
1073
+ var cellYear = viewYear;
1074
+ var cellMonth = viewMonth;
1075
+
1076
+ if (i < firstDay) {
1077
+ // Previous month
1078
+ dayNum = daysInPrev - firstDay + i + 1;
1079
+ isOutside = true;
1080
+ cellMonth = viewMonth - 1;
1081
+ if (cellMonth < 0) { cellMonth = 11; cellYear--; }
1082
+ } else if (i - firstDay >= daysInMonth) {
1083
+ // Next month
1084
+ dayNum = i - firstDay - daysInMonth + 1;
1085
+ isOutside = true;
1086
+ cellMonth = viewMonth + 1;
1087
+ if (cellMonth > 11) { cellMonth = 0; cellYear++; }
1088
+ } else {
1089
+ dayNum = i - firstDay + 1;
1090
+ }
1091
+
1092
+ cellDate = new Date(cellYear, cellMonth, dayNum);
1093
+
1094
+ var classes = 'flm-datepicker-day';
1095
+ if (isOutside) classes += ' flm-datepicker-day--outside';
1096
+ if (cellDate.getTime() === today.getTime()) classes += ' flm-datepicker-day--today';
1097
+ if (selectedDate && cellDate.getTime() === selectedDate.getTime()) classes += ' flm-datepicker-day--selected';
1098
+
1099
+ var disabled = false;
1100
+ if (minDate && cellDate < minDate) disabled = true;
1101
+ if (maxDate && cellDate > maxDate) disabled = true;
1102
+ if (disabled) classes += ' flm-datepicker-day--disabled';
1103
+
1104
+ html += '<button class="' + classes + '" data-date="' + cellYear + '-' + cellMonth + '-' + dayNum + '"' +
1105
+ (disabled ? ' disabled' : '') + '>' + dayNum + '</button>';
1106
+ }
1107
+
1108
+ html += '</div>';
1109
+ callout.innerHTML = html;
1110
+
1111
+ // Wire navigation buttons
1112
+ var prevBtn = callout.querySelector('.flm-datepicker-prev');
1113
+ var nextBtn = callout.querySelector('.flm-datepicker-next');
1114
+
1115
+ if (prevBtn) {
1116
+ // Disable prev button if at min date boundary
1117
+ if (minDate && viewYear === minDate.getFullYear() && viewMonth === minDate.getMonth()) {
1118
+ prevBtn.disabled = true;
1119
+ }
1120
+ prevBtn.addEventListener('click', function (e) {
1121
+ e.stopPropagation();
1122
+ var newMonth = viewMonth - 1;
1123
+ var newYear = viewYear;
1124
+ if (newMonth < 0) { newMonth = 11; newYear--; }
1125
+ // Don't navigate before the month containing minDate
1126
+ if (minDate) {
1127
+ var minMonth = minDate.getFullYear() * 12 + minDate.getMonth();
1128
+ var targetMonth = newYear * 12 + newMonth;
1129
+ if (targetMonth < minMonth) return;
1130
+ }
1131
+ viewMonth = newMonth;
1132
+ viewYear = newYear;
1133
+ renderCalendar();
1134
+ if (typeof FluentLMIconComponent !== 'undefined') {
1135
+ FluentLMIconComponent.init(callout);
1136
+ }
1137
+ });
1138
+ }
1139
+ if (nextBtn) {
1140
+ // Disable next button if at max date boundary
1141
+ if (maxDate && viewYear === maxDate.getFullYear() && viewMonth === maxDate.getMonth()) {
1142
+ nextBtn.disabled = true;
1143
+ }
1144
+ nextBtn.addEventListener('click', function (e) {
1145
+ e.stopPropagation();
1146
+ var newMonth = viewMonth + 1;
1147
+ var newYear = viewYear;
1148
+ if (newMonth > 11) { newMonth = 0; newYear++; }
1149
+ // Don't navigate past the month containing maxDate
1150
+ if (maxDate) {
1151
+ var maxMonth = maxDate.getFullYear() * 12 + maxDate.getMonth();
1152
+ var targetMonth = newYear * 12 + newMonth;
1153
+ if (targetMonth > maxMonth) return;
1154
+ }
1155
+ viewMonth = newMonth;
1156
+ viewYear = newYear;
1157
+ renderCalendar();
1158
+ if (typeof FluentLMIconComponent !== 'undefined') {
1159
+ FluentLMIconComponent.init(callout);
1160
+ }
1161
+ });
1162
+ }
1163
+
1164
+ // Wire day clicks
1165
+ var days = callout.querySelectorAll('.flm-datepicker-day');
1166
+ for (var j = 0; j < days.length; j++) {
1167
+ days[j].addEventListener('click', function (e) {
1168
+ e.stopPropagation();
1169
+ var parts = this.getAttribute('data-date').split('-');
1170
+ selectedDate = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10), parseInt(parts[2], 10));
1171
+ input.value = formatDate(selectedDate);
1172
+ close();
1173
+
1174
+ // Fire change event
1175
+ var evt = document.createEvent('Event');
1176
+ evt.initEvent('change', true, true);
1177
+ input.dispatchEvent(evt);
1178
+ });
1179
+ }
1180
+
1181
+ // Init icons for nav buttons
1182
+ if (typeof FluentLMIconComponent !== 'undefined') {
1183
+ FluentLMIconComponent.init(callout);
1184
+ }
1185
+ }
1186
+
1187
+ // Toggle on icon button click
1188
+ if (iconBtn) {
1189
+ iconBtn.addEventListener('click', function (e) {
1190
+ e.stopPropagation();
1191
+ if (isOpen()) {
1192
+ close();
1193
+ } else {
1194
+ open();
1195
+ }
1196
+ });
1197
+ }
1198
+
1199
+ // Open on input focus
1200
+ input.addEventListener('focus', function () {
1201
+ if (!isOpen()) open();
1202
+ });
1203
+
1204
+ // Click outside to dismiss
1205
+ document.addEventListener('click', function (e) {
1206
+ if (!el.contains(e.target) && isOpen()) {
1207
+ close();
1208
+ }
1209
+ });
1210
+
1211
+ // Keyboard: Escape closes
1212
+ input.addEventListener('keydown', function (e) {
1213
+ if (e.key === 'Escape' || e.keyCode === 27) {
1214
+ close();
1215
+ }
1216
+ });
1217
+
1218
+ // Close when another picker opens
1219
+ document.addEventListener('flm-dismiss-pickers', function (e) {
1220
+ if (e.detail.source !== el && isOpen()) close();
1221
+ });
1222
+
1223
+ el.setAttribute('data-datepicker-wired', 'true');
1224
+ }
1225
+
1226
+ return { init: init };
1227
+ })();
1228
+
1229
+ /**
1230
+ * Dialog component JS — open/close, Escape key, overlay click dismiss.
1231
+ *
1232
+ * Usage:
1233
+ * FluentLMDialogComponent.open('my-dialog')
1234
+ * FluentLMDialogComponent.close('my-dialog')
1235
+ *
1236
+ * Or wire a trigger button:
1237
+ * <button data-dialog-open="my-dialog">Open</button>
1238
+ */
1239
+ var FluentLMDialogComponent = (function () {
1240
+ 'use strict';
1241
+
1242
+ function init(root) {
1243
+ var doc = root || document;
1244
+
1245
+ // Wire trigger buttons
1246
+ var triggers = doc.querySelectorAll('[data-dialog-open]');
1247
+ for (var i = 0; i < triggers.length; i++) {
1248
+ wireOpen(triggers[i]);
1249
+ }
1250
+
1251
+ // Wire close buttons inside dialogs
1252
+ var closeBtns = doc.querySelectorAll('.flm-dialog-close, [data-dialog-close]');
1253
+ for (var j = 0; j < closeBtns.length; j++) {
1254
+ wireClose(closeBtns[j]);
1255
+ }
1256
+
1257
+ // Wire overlay click-to-dismiss (light dismiss)
1258
+ var overlays = doc.querySelectorAll('.flm-dialog-overlay[data-light-dismiss]');
1259
+ for (var k = 0; k < overlays.length; k++) {
1260
+ wireOverlayDismiss(overlays[k]);
1261
+ }
1262
+ }
1263
+
1264
+ function wireOpen(btn) {
1265
+ if (btn.getAttribute('data-dialog-wired')) return;
1266
+ btn.addEventListener('click', function () {
1267
+ var id = btn.getAttribute('data-dialog-open');
1268
+ open(id);
1269
+ });
1270
+ btn.setAttribute('data-dialog-wired', 'true');
1271
+ }
1272
+
1273
+ function wireClose(btn) {
1274
+ if (btn.getAttribute('data-dialog-wired')) return;
1275
+ btn.addEventListener('click', function () {
1276
+ var overlay = btn.closest('.flm-dialog-overlay');
1277
+ if (overlay) {
1278
+ closeOverlay(overlay);
1279
+ }
1280
+ });
1281
+ btn.setAttribute('data-dialog-wired', 'true');
1282
+ }
1283
+
1284
+ function wireOverlayDismiss(overlay) {
1285
+ if (overlay.getAttribute('data-dismiss-wired')) return;
1286
+ overlay.addEventListener('click', function (e) {
1287
+ if (e.target === overlay) {
1288
+ closeOverlay(overlay);
1289
+ }
1290
+ });
1291
+ overlay.setAttribute('data-dismiss-wired', 'true');
1292
+ }
1293
+
1294
+ function open(id) {
1295
+ var overlay = document.getElementById(id);
1296
+ if (!overlay) return;
1297
+ overlay.classList.add('flm-dialog-overlay--open');
1298
+ document.body.style.overflow = 'hidden';
1299
+
1300
+ // Escape key listener
1301
+ var escHandler = function (e) {
1302
+ if (e.key === 'Escape') {
1303
+ closeOverlay(overlay);
1304
+ document.removeEventListener('keydown', escHandler);
1305
+ }
1306
+ };
1307
+ document.addEventListener('keydown', escHandler);
1308
+ overlay._escHandler = escHandler;
1309
+
1310
+ // Focus first focusable element
1311
+ setTimeout(function () {
1312
+ var focusable = overlay.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
1313
+ if (focusable) focusable.focus();
1314
+ }, 50);
1315
+ }
1316
+
1317
+ function close(id) {
1318
+ var overlay = document.getElementById(id);
1319
+ if (overlay) closeOverlay(overlay);
1320
+ }
1321
+
1322
+ function closeOverlay(overlay) {
1323
+ overlay.classList.remove('flm-dialog-overlay--open');
1324
+ document.body.style.overflow = '';
1325
+ if (overlay._escHandler) {
1326
+ document.removeEventListener('keydown', overlay._escHandler);
1327
+ delete overlay._escHandler;
1328
+ }
1329
+ }
1330
+
1331
+ return { init: init, open: open, close: close };
1332
+ })();
1333
+
1334
+ /**
1335
+ * DocumentCard component JS — stub for icon injection via FluentLMIconComponent.
1336
+ */
1337
+ var FluentLMDocumentCardComponent = (function () {
1338
+ 'use strict';
1339
+
1340
+ function init(root) {
1341
+ var doc = root || document;
1342
+
1343
+ var cards = doc.querySelectorAll('.flm-documentcard');
1344
+ for (var i = 0; i < cards.length; i++) {
1345
+ wireCard(cards[i]);
1346
+ }
1347
+ }
1348
+
1349
+ function wireCard(card) {
1350
+ if (card.getAttribute('data-documentcard-wired')) return;
1351
+
1352
+ // Icons are handled by FluentLMIconComponent.
1353
+ // Wire up any action buttons for focus management.
1354
+ var actions = card.querySelectorAll('.flm-documentcard-actions .flm-button');
1355
+ for (var i = 0; i < actions.length; i++) {
1356
+ actions[i].addEventListener('click', function (e) {
1357
+ e.stopPropagation();
1358
+ });
1359
+ }
1360
+
1361
+ card.setAttribute('data-documentcard-wired', 'true');
1362
+ }
1363
+
1364
+ return { init: init };
1365
+ })();
1366
+
1367
+ /**
1368
+ * Dropdown component JS — custom dropdown with keyboard navigation.
1369
+ *
1370
+ * Usage:
1371
+ * <div class="flm-dropdown">
1372
+ * <label class="flm-label" for="dd1">Country</label>
1373
+ * <button class="flm-dropdown-trigger" id="dd1">
1374
+ * <span class="flm-dropdown-title flm-dropdown-title--placeholder">Select…</span>
1375
+ * <span class="flm-dropdown-caret" data-icon="ChevronDown"></span>
1376
+ * </button>
1377
+ * <div class="flm-dropdown-listbox">
1378
+ * <div class="flm-dropdown-option" data-value="us">United States</div>
1379
+ * <div class="flm-dropdown-option" data-value="gb">United Kingdom</div>
1380
+ * </div>
1381
+ * </div>
1382
+ *
1383
+ * Sets data-value on the root .flm-dropdown when an option is selected.
1384
+ * A hidden <input class="flm-dropdown-value"> is updated if present.
1385
+ */
1386
+ var FluentLMDropdownComponent = (function () {
1387
+ 'use strict';
1388
+
1389
+ function init(root) {
1390
+ var doc = root || document;
1391
+
1392
+ var dropdowns = doc.querySelectorAll('.flm-dropdown');
1393
+ for (var i = 0; i < dropdowns.length; i++) {
1394
+ wireDropdown(dropdowns[i]);
1395
+ }
1396
+ }
1397
+
1398
+ function wireDropdown(el) {
1399
+ if (el.getAttribute('data-dropdown-wired')) return;
1400
+ if (el.classList.contains('flm-dropdown--disabled')) return;
1401
+
1402
+ var trigger = el.querySelector('.flm-dropdown-trigger');
1403
+ var listbox = el.querySelector('.flm-dropdown-listbox');
1404
+
1405
+ if (!trigger || !listbox) return;
1406
+
1407
+ var titleEl = trigger.querySelector('.flm-dropdown-title');
1408
+ var hiddenInput = el.querySelector('.flm-dropdown-value');
1409
+ var placeholder = titleEl ? titleEl.textContent : '';
1410
+ var highlighted = -1;
1411
+
1412
+ function getOptions() {
1413
+ return listbox.querySelectorAll('.flm-dropdown-option:not(.flm-dropdown-option--disabled)');
1414
+ }
1415
+
1416
+ function isOpen() {
1417
+ return listbox.classList.contains('flm-dropdown-listbox--open');
1418
+ }
1419
+
1420
+ function open() {
1421
+ document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
1422
+ listbox.classList.add('flm-dropdown-listbox--open');
1423
+ highlighted = -1;
1424
+
1425
+ // Highlight current selection
1426
+ var opts = getOptions();
1427
+ for (var i = 0; i < opts.length; i++) {
1428
+ if (opts[i].classList.contains('flm-dropdown-option--selected')) {
1429
+ highlighted = i;
1430
+ opts[i].classList.add('flm-dropdown-option--highlighted');
1431
+ opts[i].scrollIntoView({ block: 'nearest' });
1432
+ break;
1433
+ }
1434
+ }
1435
+
1436
+ flipIfNeeded();
1437
+ }
1438
+
1439
+ function close() {
1440
+ listbox.classList.remove('flm-dropdown-listbox--open', 'flm-dropdown-listbox--above');
1441
+ highlighted = -1;
1442
+ clearHighlight();
1443
+ }
1444
+
1445
+ function flipIfNeeded() {
1446
+ setTimeout(function () {
1447
+ var rect = listbox.getBoundingClientRect();
1448
+ if (rect.bottom > window.innerHeight) {
1449
+ listbox.classList.add('flm-dropdown-listbox--above');
1450
+ } else {
1451
+ listbox.classList.remove('flm-dropdown-listbox--above');
1452
+ }
1453
+ }, 0);
1454
+ }
1455
+
1456
+ function clearHighlight() {
1457
+ var opts = listbox.querySelectorAll('.flm-dropdown-option--highlighted');
1458
+ for (var i = 0; i < opts.length; i++) {
1459
+ opts[i].classList.remove('flm-dropdown-option--highlighted');
1460
+ }
1461
+ }
1462
+
1463
+ function setHighlight(idx) {
1464
+ clearHighlight();
1465
+ var opts = getOptions();
1466
+ if (idx >= 0 && idx < opts.length) {
1467
+ highlighted = idx;
1468
+ opts[idx].classList.add('flm-dropdown-option--highlighted');
1469
+ opts[idx].scrollIntoView({ block: 'nearest' });
1470
+ }
1471
+ }
1472
+
1473
+ function selectOption(opt) {
1474
+ // Clear previous
1475
+ var prev = listbox.querySelectorAll('.flm-dropdown-option--selected');
1476
+ for (var i = 0; i < prev.length; i++) {
1477
+ prev[i].classList.remove('flm-dropdown-option--selected');
1478
+ }
1479
+
1480
+ opt.classList.add('flm-dropdown-option--selected');
1481
+
1482
+ var value = opt.getAttribute('data-value') || opt.textContent.trim();
1483
+ var text = opt.textContent.trim();
1484
+
1485
+ if (titleEl) {
1486
+ titleEl.textContent = text;
1487
+ titleEl.classList.remove('flm-dropdown-title--placeholder');
1488
+ }
1489
+
1490
+ el.setAttribute('data-value', value);
1491
+
1492
+ if (hiddenInput) {
1493
+ hiddenInput.value = value;
1494
+ }
1495
+
1496
+ close();
1497
+
1498
+ // Fire change event
1499
+ var evt = document.createEvent('Event');
1500
+ evt.initEvent('change', true, true);
1501
+ el.dispatchEvent(evt);
1502
+ }
1503
+
1504
+ // Toggle on click
1505
+ trigger.addEventListener('click', function (e) {
1506
+ e.stopPropagation();
1507
+ if (isOpen()) {
1508
+ close();
1509
+ } else {
1510
+ open();
1511
+ }
1512
+ });
1513
+
1514
+ // Keyboard navigation
1515
+ trigger.addEventListener('keydown', function (e) {
1516
+ var opts = getOptions();
1517
+ var len = opts.length;
1518
+
1519
+ if (e.key === 'ArrowDown' || e.keyCode === 40) {
1520
+ e.preventDefault();
1521
+ if (!isOpen()) open();
1522
+ setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
1523
+ } else if (e.key === 'ArrowUp' || e.keyCode === 38) {
1524
+ e.preventDefault();
1525
+ if (!isOpen()) open();
1526
+ setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
1527
+ } else if (e.key === 'Enter' || e.keyCode === 13 || e.key === ' ' || e.keyCode === 32) {
1528
+ e.preventDefault();
1529
+ if (!isOpen()) {
1530
+ open();
1531
+ } else if (highlighted >= 0 && highlighted < len) {
1532
+ selectOption(opts[highlighted]);
1533
+ }
1534
+ } else if (e.key === 'Escape' || e.keyCode === 27) {
1535
+ close();
1536
+ }
1537
+ });
1538
+
1539
+ // Option click
1540
+ listbox.addEventListener('click', function (e) {
1541
+ var opt = e.target.closest('.flm-dropdown-option');
1542
+ if (opt && !opt.classList.contains('flm-dropdown-option--disabled')) {
1543
+ selectOption(opt);
1544
+ }
1545
+ });
1546
+
1547
+ // Click outside
1548
+ document.addEventListener('click', function (e) {
1549
+ if (!el.contains(e.target) && isOpen()) {
1550
+ close();
1551
+ }
1552
+ });
1553
+
1554
+ // Close when another picker opens
1555
+ document.addEventListener('flm-dismiss-pickers', function (e) {
1556
+ if (e.detail.source !== el && isOpen()) close();
1557
+ });
1558
+
1559
+ el.setAttribute('data-dropdown-wired', 'true');
1560
+ }
1561
+
1562
+ return { init: init };
1563
+ })();
1564
+
1565
+ /**
1566
+ * Facepile component JS — hides excess coins and injects +N overflow chip.
1567
+ */
1568
+ var FluentLMFacepileComponent = (function () {
1569
+ 'use strict';
1570
+
1571
+ function init(root) {
1572
+ var doc = root || document;
1573
+
1574
+ var facepiles = doc.querySelectorAll('.flm-facepile');
1575
+ for (var i = 0; i < facepiles.length; i++) {
1576
+ wireFacepile(facepiles[i]);
1577
+ }
1578
+ }
1579
+
1580
+ function wireFacepile(el) {
1581
+ if (el.getAttribute('data-facepile-wired')) return;
1582
+
1583
+ var max = parseInt(el.getAttribute('data-max'), 10);
1584
+ if (!max || isNaN(max)) {
1585
+ el.setAttribute('data-facepile-wired', 'true');
1586
+ return;
1587
+ }
1588
+
1589
+ var members = el.querySelectorAll('.flm-facepile-member');
1590
+ if (members.length <= max) {
1591
+ el.setAttribute('data-facepile-wired', 'true');
1592
+ return;
1593
+ }
1594
+
1595
+ var overflow = members.length - max;
1596
+
1597
+ // Hide excess members
1598
+ for (var i = max; i < members.length; i++) {
1599
+ members[i].style.display = 'none';
1600
+ }
1601
+
1602
+ // Remove existing overflow chip if any
1603
+ var existing = el.querySelector('.flm-facepile-overflow');
1604
+ if (existing) {
1605
+ existing.parentNode.removeChild(existing);
1606
+ }
1607
+
1608
+ // Inject overflow chip
1609
+ var chip = document.createElement('span');
1610
+ chip.className = 'flm-facepile-overflow';
1611
+ chip.textContent = '+' + overflow;
1612
+
1613
+ // Insert after last visible member
1614
+ var lastVisible = members[max - 1];
1615
+ if (lastVisible.nextSibling) {
1616
+ el.insertBefore(chip, lastVisible.nextSibling);
1617
+ } else {
1618
+ el.appendChild(chip);
1619
+ }
1620
+
1621
+ el.setAttribute('data-facepile-wired', 'true');
1622
+ }
1623
+
1624
+ return { init: init };
1625
+ })();
1626
+
1627
+ /**
1628
+ * GroupedList component JS — collapsible groups with chevron rotation.
1629
+ */
1630
+ var FluentLMGroupedListComponent = (function () {
1631
+ 'use strict';
1632
+
1633
+ function init(root) {
1634
+ var doc = root || document;
1635
+
1636
+ var headers = doc.querySelectorAll('.flm-groupedlist-header');
1637
+ for (var i = 0; i < headers.length; i++) {
1638
+ wireHeader(headers[i]);
1639
+ }
1640
+ }
1641
+
1642
+ function wireHeader(header) {
1643
+ if (header.getAttribute('data-groupedlist-wired')) return;
1644
+
1645
+ header.addEventListener('click', function () {
1646
+ var items = header.nextElementSibling;
1647
+ if (!items || !items.classList.contains('flm-groupedlist-items')) return;
1648
+
1649
+ var chevron = header.querySelector('.flm-groupedlist-chevron');
1650
+ var collapsed = items.classList.contains('flm-groupedlist-items--collapsed');
1651
+
1652
+ if (collapsed) {
1653
+ items.classList.remove('flm-groupedlist-items--collapsed');
1654
+ if (chevron) chevron.classList.add('flm-groupedlist-chevron--expanded');
1655
+ } else {
1656
+ items.classList.add('flm-groupedlist-items--collapsed');
1657
+ if (chevron) chevron.classList.remove('flm-groupedlist-chevron--expanded');
1658
+ }
1659
+ });
1660
+
1661
+ header.setAttribute('data-groupedlist-wired', 'true');
1662
+ }
1663
+
1664
+ return { init: init };
1665
+ })();
1666
+
1667
+ /**
1668
+ * HoverCard component JS — two-phase hover: compact after 500ms, expanded after 1500ms.
1669
+ *
1670
+ * Usage:
1671
+ * <span class="flm-hovercard-host" data-hovercard-id="hc1">Hover over me</span>
1672
+ * <div class="flm-hovercard" id="hc1">
1673
+ * <div class="flm-hovercard-compact">Compact content</div>
1674
+ * <div class="flm-hovercard-expanded">Expanded content</div>
1675
+ * </div>
1676
+ */
1677
+ var FluentLMHoverCardComponent = (function () {
1678
+ 'use strict';
1679
+
1680
+ function init(root) {
1681
+ var doc = root || document;
1682
+
1683
+ var hosts = doc.querySelectorAll('[data-hovercard-id]');
1684
+ for (var i = 0; i < hosts.length; i++) {
1685
+ wireHost(hosts[i]);
1686
+ }
1687
+ }
1688
+
1689
+ function wireHost(host) {
1690
+ if (host.getAttribute('data-hovercard-wired')) return;
1691
+
1692
+ var showTimer = null;
1693
+ var expandTimer = null;
1694
+ var card = null;
1695
+
1696
+ function getCard() {
1697
+ if (!card) {
1698
+ var id = host.getAttribute('data-hovercard-id');
1699
+ card = document.getElementById(id);
1700
+ }
1701
+ return card;
1702
+ }
1703
+
1704
+ host.addEventListener('mouseenter', function () {
1705
+ showTimer = setTimeout(function () {
1706
+ showCompact(getCard(), host);
1707
+
1708
+ expandTimer = setTimeout(function () {
1709
+ showExpanded(getCard());
1710
+ }, 1000); // 1000ms after compact = 1500ms total
1711
+ }, 500);
1712
+ });
1713
+
1714
+ host.addEventListener('mouseleave', function () {
1715
+ clearTimeout(showTimer);
1716
+ clearTimeout(expandTimer);
1717
+
1718
+ // Delay hide to allow mouse to move to card
1719
+ var c = getCard();
1720
+ if (c) {
1721
+ setTimeout(function () {
1722
+ if (!c._hovered) {
1723
+ hide(c);
1724
+ }
1725
+ }, 100);
1726
+ }
1727
+ });
1728
+
1729
+ // Keep card open while mouse is over it
1730
+ var hoverCardEl = getCard();
1731
+ if (hoverCardEl) {
1732
+ hoverCardEl.addEventListener('mouseenter', function () {
1733
+ hoverCardEl._hovered = true;
1734
+ });
1735
+
1736
+ hoverCardEl.addEventListener('mouseleave', function () {
1737
+ hoverCardEl._hovered = false;
1738
+ clearTimeout(expandTimer);
1739
+ hide(hoverCardEl);
1740
+ });
1741
+ }
1742
+
1743
+ host.setAttribute('data-hovercard-wired', 'true');
1744
+ }
1745
+
1746
+ function showCompact(card, host) {
1747
+ if (!card) return;
1748
+
1749
+ var rect = host.getBoundingClientRect();
1750
+ var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
1751
+ var scrollY = window.pageYOffset || document.documentElement.scrollTop;
1752
+
1753
+ card.style.position = 'absolute';
1754
+ card.style.left = rect.left + scrollX + 'px';
1755
+ card.style.top = (rect.bottom + scrollY + 4) + 'px';
1756
+
1757
+ card.classList.add('flm-hovercard--visible');
1758
+
1759
+ // Flip if off-screen
1760
+ setTimeout(function () {
1761
+ var cRect = card.getBoundingClientRect();
1762
+ if (cRect.bottom > window.innerHeight) {
1763
+ card.style.top = (rect.top + scrollY - cRect.height - 4) + 'px';
1764
+ }
1765
+ if (cRect.right > window.innerWidth) {
1766
+ card.style.left = (rect.right + scrollX - cRect.width) + 'px';
1767
+ }
1768
+ }, 0);
1769
+ }
1770
+
1771
+ function showExpanded(card) {
1772
+ if (!card) return;
1773
+ var expanded = card.querySelector('.flm-hovercard-expanded');
1774
+ if (expanded) {
1775
+ expanded.classList.add('flm-hovercard-expanded--visible');
1776
+ }
1777
+ }
1778
+
1779
+ function hide(card) {
1780
+ if (!card) return;
1781
+ card.classList.remove('flm-hovercard--visible');
1782
+ var expanded = card.querySelector('.flm-hovercard-expanded');
1783
+ if (expanded) {
1784
+ expanded.classList.remove('flm-hovercard-expanded--visible');
1785
+ }
1786
+ }
1787
+
1788
+ return { init: init };
1789
+ })();
1790
+
1791
+ /**
1792
+ * Icon component — resolves data-icon attributes to inline SVG.
1793
+ */
1794
+ var FluentLMIconComponent = (function () {
1795
+ 'use strict';
1796
+
1797
+ function init(root) {
1798
+ var els = (root || document).querySelectorAll('[data-icon]');
1799
+ for (var i = 0; i < els.length; i++) {
1800
+ render(els[i]);
1801
+ }
1802
+ }
1803
+
1804
+ function render(el) {
1805
+ // Skip if already rendered
1806
+ if (el.getAttribute('data-icon-rendered')) return;
1807
+
1808
+ var name = el.getAttribute('data-icon');
1809
+ if (!name) return;
1810
+
1811
+ var svg = FluentIcons.getSvg(name);
1812
+ if (!svg) return;
1813
+
1814
+ // For .flm-icon elements or inline icon elements (i, span with data-icon only), replace contents
1815
+ if (el.classList.contains('flm-icon') || ((el.tagName === 'I' || el.tagName === 'SPAN') && el.childNodes.length === 0)) {
1816
+ el.innerHTML = '';
1817
+ el.appendChild(svg);
1818
+ }
1819
+ // For buttons / other elements, prepend icon
1820
+ else if (el.classList.contains('flm-button') || el.tagName === 'BUTTON' || el.tagName === 'A') {
1821
+ el.insertBefore(svg, el.firstChild);
1822
+ // Add a small space text node if there's text content after the icon
1823
+ if (el.childNodes.length > 1 && !el.classList.contains('flm-button--icon')) {
1824
+ svg.style.marginRight = '4px';
1825
+ }
1826
+ }
1827
+
1828
+ el.setAttribute('data-icon-rendered', 'true');
1829
+ }
1830
+
1831
+ return { init: init, render: render };
1832
+ })();
1833
+
1834
+ /**
1835
+ * MessageBar component JS — auto-injects status icon and wires dismiss button.
1836
+ */
1837
+ var FluentLMMessageBarComponent = (function () {
1838
+ 'use strict';
1839
+
1840
+ // Map messagebar type to icon name
1841
+ var typeIconMap = {
1842
+ 'info': 'Info',
1843
+ 'success': 'Completed',
1844
+ 'warning': 'Warning',
1845
+ 'severeWarning': 'Warning',
1846
+ 'error': 'ErrorBadge',
1847
+ 'blocked': 'Blocked'
1848
+ };
1849
+
1850
+ function getType(el) {
1851
+ var classes = el.className;
1852
+ var types = Object.keys(typeIconMap);
1853
+ for (var i = 0; i < types.length; i++) {
1854
+ if (classes.indexOf('flm-messagebar--' + types[i]) !== -1) {
1855
+ return types[i];
1856
+ }
1857
+ }
1858
+ return 'info';
1859
+ }
1860
+
1861
+ function init(root) {
1862
+ var els = (root || document).querySelectorAll('.flm-messagebar');
1863
+ for (var i = 0; i < els.length; i++) {
1864
+ render(els[i]);
1865
+ }
1866
+ }
1867
+
1868
+ function render(el) {
1869
+ if (el.getAttribute('data-messagebar-rendered')) return;
1870
+
1871
+ var type = getType(el);
1872
+
1873
+ // Auto-inject icon if none exists
1874
+ if (!el.querySelector('.flm-messagebar-icon')) {
1875
+ var iconName = typeIconMap[type];
1876
+ var svg = FluentIcons.getSvg(iconName);
1877
+ if (svg) {
1878
+ var iconSpan = document.createElement('span');
1879
+ iconSpan.className = 'flm-messagebar-icon';
1880
+ iconSpan.appendChild(svg);
1881
+ el.insertBefore(iconSpan, el.firstChild);
1882
+ }
1883
+ }
1884
+
1885
+ // Wrap bare text content in .flm-messagebar-text if not already wrapped
1886
+ if (!el.querySelector('.flm-messagebar-text')) {
1887
+ var children = Array.prototype.slice.call(el.childNodes);
1888
+ var textWrapper = document.createElement('span');
1889
+ textWrapper.className = 'flm-messagebar-text';
1890
+ for (var i = 0; i < children.length; i++) {
1891
+ var child = children[i];
1892
+ if (!child.classList ||
1893
+ (!child.classList.contains('flm-messagebar-icon') &&
1894
+ !child.classList.contains('flm-messagebar-actions') &&
1895
+ !child.classList.contains('flm-messagebar-dismiss'))) {
1896
+ textWrapper.appendChild(child);
1897
+ }
1898
+ }
1899
+ // Insert text wrapper after the icon
1900
+ var icon = el.querySelector('.flm-messagebar-icon');
1901
+ if (icon) {
1902
+ icon.after(textWrapper);
1903
+ } else {
1904
+ el.insertBefore(textWrapper, el.firstChild);
1905
+ }
1906
+ }
1907
+
1908
+ // Wire up dismiss button
1909
+ var dismiss = el.querySelector('.flm-messagebar-dismiss');
1910
+ if (dismiss) {
1911
+ dismiss.addEventListener('click', function () {
1912
+ el.style.display = 'none';
1913
+ });
1914
+ }
1915
+
1916
+ // ARIA
1917
+ el.setAttribute('role', 'status');
1918
+ if (type === 'error' || type === 'blocked' || type === 'severeWarning') {
1919
+ el.setAttribute('role', 'alert');
1920
+ }
1921
+
1922
+ el.setAttribute('data-messagebar-rendered', 'true');
1923
+ }
1924
+
1925
+ return { init: init };
1926
+ })();
1927
+
1928
+ /**
1929
+ * Modal component JS — open/close, Escape key, overlay dismiss.
1930
+ *
1931
+ * Usage:
1932
+ * FluentLMModalComponent.open('my-modal')
1933
+ * FluentLMModalComponent.close('my-modal')
1934
+ *
1935
+ * Trigger: <button data-modal-open="my-modal">Open</button>
1936
+ */
1937
+ var FluentLMModalComponent = (function () {
1938
+ 'use strict';
1939
+
1940
+ function init(root) {
1941
+ var doc = root || document;
1942
+
1943
+ var triggers = doc.querySelectorAll('[data-modal-open]');
1944
+ for (var i = 0; i < triggers.length; i++) {
1945
+ wireTrigger(triggers[i]);
1946
+ }
1947
+
1948
+ var closeBtns = doc.querySelectorAll('[data-modal-close]');
1949
+ for (var j = 0; j < closeBtns.length; j++) {
1950
+ wireClose(closeBtns[j]);
1951
+ }
1952
+
1953
+ var overlays = doc.querySelectorAll('.flm-modal-overlay');
1954
+ for (var k = 0; k < overlays.length; k++) {
1955
+ wireOverlay(overlays[k]);
1956
+ }
1957
+ }
1958
+
1959
+ function wireTrigger(btn) {
1960
+ if (btn.getAttribute('data-modal-wired')) return;
1961
+ btn.addEventListener('click', function () {
1962
+ open(btn.getAttribute('data-modal-open'));
1963
+ });
1964
+ btn.setAttribute('data-modal-wired', 'true');
1965
+ }
1966
+
1967
+ function wireClose(btn) {
1968
+ if (btn.getAttribute('data-modal-wired')) return;
1969
+ btn.addEventListener('click', function () {
1970
+ var overlay = btn.closest('.flm-modal-overlay');
1971
+ if (overlay) closeOverlay(overlay);
1972
+ });
1973
+ btn.setAttribute('data-modal-wired', 'true');
1974
+ }
1975
+
1976
+ function wireOverlay(overlay) {
1977
+ if (overlay.getAttribute('data-modal-overlay-wired')) return;
1978
+ // Only light-dismiss if not blocking
1979
+ if (overlay.hasAttribute('data-light-dismiss')) {
1980
+ overlay.addEventListener('click', function (e) {
1981
+ if (e.target === overlay) closeOverlay(overlay);
1982
+ });
1983
+ }
1984
+ overlay.setAttribute('data-modal-overlay-wired', 'true');
1985
+ }
1986
+
1987
+ function open(id) {
1988
+ var overlay = document.getElementById(id);
1989
+ if (!overlay) return;
1990
+ overlay.classList.add('flm-modal-overlay--open');
1991
+ document.body.style.overflow = 'hidden';
1992
+
1993
+ var escHandler = function (e) {
1994
+ if (e.key === 'Escape' && !overlay.hasAttribute('data-blocking')) {
1995
+ closeOverlay(overlay);
1996
+ document.removeEventListener('keydown', escHandler);
1997
+ }
1998
+ };
1999
+ document.addEventListener('keydown', escHandler);
2000
+ overlay._escHandler = escHandler;
2001
+ }
2002
+
2003
+ function close(id) {
2004
+ var overlay = document.getElementById(id);
2005
+ if (overlay) closeOverlay(overlay);
2006
+ }
2007
+
2008
+ function closeOverlay(overlay) {
2009
+ overlay.classList.remove('flm-modal-overlay--open');
2010
+ document.body.style.overflow = '';
2011
+ if (overlay._escHandler) {
2012
+ document.removeEventListener('keydown', overlay._escHandler);
2013
+ delete overlay._escHandler;
2014
+ }
2015
+ }
2016
+
2017
+ return { init: init, open: open, close: close };
2018
+ })();
2019
+
2020
+ /**
2021
+ * Nav component JS — collapsible groups and active link tracking.
2022
+ */
2023
+ var FluentLMNavComponent = (function () {
2024
+ 'use strict';
2025
+
2026
+ function init(root) {
2027
+ var doc = root || document;
2028
+
2029
+ // Wire collapsible group headers
2030
+ var headers = doc.querySelectorAll('.flm-nav-group-header');
2031
+ for (var i = 0; i < headers.length; i++) {
2032
+ wireGroupHeader(headers[i]);
2033
+ }
2034
+
2035
+ // Wire nav link clicks
2036
+ var links = doc.querySelectorAll('.flm-nav-link');
2037
+ for (var j = 0; j < links.length; j++) {
2038
+ wireLink(links[j]);
2039
+ }
2040
+ }
2041
+
2042
+ function wireGroupHeader(header) {
2043
+ if (header.getAttribute('data-nav-wired')) return;
2044
+
2045
+ header.addEventListener('click', function () {
2046
+ var items = header.nextElementSibling;
2047
+ if (!items || !items.classList.contains('flm-nav-group-items')) return;
2048
+
2049
+ var chevron = header.querySelector('.flm-nav-chevron');
2050
+ var collapsed = items.classList.contains('flm-nav-group-items--collapsed');
2051
+
2052
+ if (collapsed) {
2053
+ items.classList.remove('flm-nav-group-items--collapsed');
2054
+ if (chevron) chevron.classList.add('flm-nav-chevron--expanded');
2055
+ } else {
2056
+ items.classList.add('flm-nav-group-items--collapsed');
2057
+ if (chevron) chevron.classList.remove('flm-nav-chevron--expanded');
2058
+ }
2059
+ });
2060
+
2061
+ header.setAttribute('data-nav-wired', 'true');
2062
+ }
2063
+
2064
+ function wireLink(link) {
2065
+ if (link.getAttribute('data-nav-wired')) return;
2066
+
2067
+ link.addEventListener('click', function () {
2068
+ // Remove active from all links in this nav
2069
+ var nav = link.closest('.flm-nav');
2070
+ if (nav) {
2071
+ var allLinks = nav.querySelectorAll('.flm-nav-link');
2072
+ for (var i = 0; i < allLinks.length; i++) {
2073
+ allLinks[i].classList.remove('flm-nav-link--active');
2074
+ }
2075
+ }
2076
+ link.classList.add('flm-nav-link--active');
2077
+ });
2078
+
2079
+ link.setAttribute('data-nav-wired', 'true');
2080
+ }
2081
+
2082
+ return { init: init };
2083
+ })();
2084
+
2085
+ /**
2086
+ * OverflowSet component JS — responsive container that hides items into
2087
+ * an overflow "..." menu.
2088
+ *
2089
+ * Usage:
2090
+ * <div class="flm-overflowset">
2091
+ * <div class="flm-overflowset-items">
2092
+ * <button class="flm-overflowset-item" data-label="New" data-icon="Add">New</button>
2093
+ * <button class="flm-overflowset-item" data-label="Edit" data-icon="Edit">Edit</button>
2094
+ * </div>
2095
+ * <button class="flm-overflowset-overflow" aria-label="More items">...</button>
2096
+ * <div class="flm-overflowset-far">
2097
+ * <button class="flm-overflowset-item" data-label="Settings">Settings</button>
2098
+ * </div>
2099
+ * </div>
2100
+ *
2101
+ * Attributes:
2102
+ * data-overflow-order="N" on items — higher N overflows first.
2103
+ * data-label — label shown in overflow menu.
2104
+ * data-icon — icon name shown in overflow menu.
2105
+ */
2106
+ var FluentLMOverflowSetComponent = (function () {
2107
+ 'use strict';
2108
+
2109
+ function init(root) {
2110
+ var doc = root || document;
2111
+ var sets = doc.querySelectorAll('.flm-overflowset');
2112
+ for (var i = 0; i < sets.length; i++) {
2113
+ wireSet(sets[i]);
2114
+ }
2115
+ }
2116
+
2117
+ function wireSet(el) {
2118
+ if (el.getAttribute('data-overflowset-wired')) return;
2119
+
2120
+ var itemsContainer = el.querySelector('.flm-overflowset-items');
2121
+ var overflowBtn = el.querySelector('.flm-overflowset-overflow');
2122
+
2123
+ if (!itemsContainer) return;
2124
+
2125
+ var menuEl = null;
2126
+
2127
+ function getItems() {
2128
+ return itemsContainer.querySelectorAll('.flm-overflowset-item');
2129
+ }
2130
+
2131
+ function recalculate() {
2132
+ var items = getItems();
2133
+ var i;
2134
+
2135
+ // Show all items first to measure
2136
+ for (i = 0; i < items.length; i++) {
2137
+ items[i].classList.remove('flm-overflowset-item--hidden');
2138
+ }
2139
+ el.classList.remove('flm-overflowset--has-overflow');
2140
+
2141
+ // Get available width
2142
+ var containerWidth = itemsContainer.offsetWidth;
2143
+
2144
+ // Build array with overflow priority
2145
+ var itemData = [];
2146
+ for (i = 0; i < items.length; i++) {
2147
+ itemData.push({
2148
+ el: items[i],
2149
+ order: parseInt(items[i].getAttribute('data-overflow-order') || '0', 10),
2150
+ index: i,
2151
+ width: items[i].offsetWidth
2152
+ });
2153
+ }
2154
+
2155
+ // Sort by overflow order descending (higher order overflows first), then by reverse DOM order
2156
+ itemData.sort(function (a, b) {
2157
+ if (b.order !== a.order) return b.order - a.order;
2158
+ return b.index - a.index;
2159
+ });
2160
+
2161
+ // Measure total width
2162
+ var totalWidth = 0;
2163
+ var gap = 4; // matches var(--spacingS2) = 4px
2164
+ for (i = 0; i < items.length; i++) {
2165
+ totalWidth += items[i].offsetWidth + (i > 0 ? gap : 0);
2166
+ }
2167
+
2168
+ // Account for overflow button width if we might need it
2169
+ var overflowBtnWidth = overflowBtn ? 36 : 0; // 32px + gap
2170
+
2171
+ // Hide items until they fit
2172
+ var hiddenItems = [];
2173
+ var idx = 0;
2174
+ while (totalWidth > containerWidth && idx < itemData.length) {
2175
+ var item = itemData[idx];
2176
+ item.el.classList.add('flm-overflowset-item--hidden');
2177
+ totalWidth -= item.width + gap;
2178
+ // First time we overflow, account for the overflow button
2179
+ if (hiddenItems.length === 0) {
2180
+ totalWidth += overflowBtnWidth;
2181
+ }
2182
+ hiddenItems.push(item);
2183
+ idx++;
2184
+ }
2185
+
2186
+ if (hiddenItems.length > 0) {
2187
+ el.classList.add('flm-overflowset--has-overflow');
2188
+ }
2189
+ }
2190
+
2191
+ function buildOverflowMenu() {
2192
+ // Clean up existing menu
2193
+ if (menuEl) {
2194
+ if (menuEl.classList.contains('flm-contextmenu--visible')) {
2195
+ hideMenu();
2196
+ return;
2197
+ }
2198
+ menuEl.parentNode.removeChild(menuEl);
2199
+ menuEl = null;
2200
+ }
2201
+
2202
+ var hiddenItems = itemsContainer.querySelectorAll('.flm-overflowset-item--hidden');
2203
+ if (hiddenItems.length === 0) return;
2204
+
2205
+ menuEl = document.createElement('div');
2206
+ menuEl.className = 'flm-contextmenu';
2207
+
2208
+ for (var i = 0; i < hiddenItems.length; i++) {
2209
+ var item = hiddenItems[i];
2210
+ var label = item.getAttribute('data-label') || item.textContent.trim();
2211
+ var icon = item.getAttribute('data-icon');
2212
+
2213
+ var menuItem = document.createElement('button');
2214
+ menuItem.className = 'flm-contextmenu-item';
2215
+
2216
+ if (icon) {
2217
+ var iconSpan = document.createElement('span');
2218
+ iconSpan.className = 'flm-contextmenu-item-icon';
2219
+ var svg = typeof FluentIcons !== 'undefined' ? FluentIcons.getSvg(icon) : null;
2220
+ if (svg) {
2221
+ iconSpan.appendChild(svg);
2222
+ }
2223
+ menuItem.appendChild(iconSpan);
2224
+ }
2225
+
2226
+ var textSpan = document.createElement('span');
2227
+ textSpan.className = 'flm-contextmenu-item-text';
2228
+ textSpan.textContent = label;
2229
+ menuItem.appendChild(textSpan);
2230
+
2231
+ // Proxy click to original item
2232
+ (function (originalItem) {
2233
+ menuItem.addEventListener('click', function () {
2234
+ hideMenu();
2235
+ originalItem.click();
2236
+ });
2237
+ })(item);
2238
+
2239
+ menuEl.appendChild(menuItem);
2240
+ }
2241
+
2242
+ document.body.appendChild(menuEl);
2243
+ showMenu();
2244
+ }
2245
+
2246
+ function showMenu() {
2247
+ if (!menuEl || !overflowBtn) return;
2248
+
2249
+ var rect = overflowBtn.getBoundingClientRect();
2250
+ var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
2251
+ var scrollY = window.pageYOffset || document.documentElement.scrollTop;
2252
+
2253
+ menuEl.style.position = 'absolute';
2254
+ menuEl.style.left = rect.left + scrollX + 'px';
2255
+ menuEl.style.top = (rect.bottom + scrollY + 2) + 'px';
2256
+ menuEl.classList.add('flm-contextmenu--visible');
2257
+
2258
+ // Reposition if off-screen
2259
+ setTimeout(function () {
2260
+ var menuRect = menuEl.getBoundingClientRect();
2261
+ if (menuRect.right > window.innerWidth) {
2262
+ menuEl.style.left = (rect.right + scrollX - menuRect.width) + 'px';
2263
+ }
2264
+ if (menuRect.bottom > window.innerHeight) {
2265
+ menuEl.style.top = (rect.top + scrollY - menuRect.height - 2) + 'px';
2266
+ }
2267
+ }, 0);
2268
+
2269
+ // Click outside to close
2270
+ var outsideHandler = function (e) {
2271
+ if (!menuEl.contains(e.target) && e.target !== overflowBtn) {
2272
+ hideMenu();
2273
+ document.removeEventListener('click', outsideHandler);
2274
+ }
2275
+ };
2276
+ setTimeout(function () {
2277
+ document.addEventListener('click', outsideHandler);
2278
+ }, 0);
2279
+ menuEl._outsideHandler = outsideHandler;
2280
+ }
2281
+
2282
+ function hideMenu() {
2283
+ if (!menuEl) return;
2284
+ menuEl.classList.remove('flm-contextmenu--visible');
2285
+ if (menuEl._outsideHandler) {
2286
+ document.removeEventListener('click', menuEl._outsideHandler);
2287
+ delete menuEl._outsideHandler;
2288
+ }
2289
+ // Remove from DOM after transition
2290
+ setTimeout(function () {
2291
+ if (menuEl && menuEl.parentNode) {
2292
+ menuEl.parentNode.removeChild(menuEl);
2293
+ }
2294
+ menuEl = null;
2295
+ }, 200);
2296
+ }
2297
+
2298
+ // Wire overflow button click
2299
+ if (overflowBtn) {
2300
+ overflowBtn.addEventListener('click', function (e) {
2301
+ e.stopPropagation();
2302
+ buildOverflowMenu();
2303
+ });
2304
+ }
2305
+
2306
+ // Observe size changes
2307
+ if (typeof ResizeObserver !== 'undefined') {
2308
+ var observer = new ResizeObserver(function () {
2309
+ recalculate();
2310
+ });
2311
+ observer.observe(el);
2312
+ } else {
2313
+ window.addEventListener('resize', recalculate);
2314
+ }
2315
+
2316
+ // Initial calculation
2317
+ recalculate();
2318
+
2319
+ el.setAttribute('data-overflowset-wired', 'true');
2320
+ }
2321
+
2322
+ return { init: init };
2323
+ })();
2324
+
2325
+ /**
2326
+ * Panel component JS — slide-in/out, Escape key, overlay dismiss.
2327
+ *
2328
+ * Usage:
2329
+ * FluentLMPanelComponent.open('my-panel')
2330
+ * FluentLMPanelComponent.close('my-panel')
2331
+ *
2332
+ * Trigger: <button data-panel-open="my-panel">Open</button>
2333
+ */
2334
+ var FluentLMPanelComponent = (function () {
2335
+ 'use strict';
2336
+
2337
+ function init(root) {
2338
+ var doc = root || document;
2339
+
2340
+ var triggers = doc.querySelectorAll('[data-panel-open]');
2341
+ for (var i = 0; i < triggers.length; i++) {
2342
+ wireTrigger(triggers[i]);
2343
+ }
2344
+
2345
+ var closeBtns = doc.querySelectorAll('.flm-panel-close, [data-panel-close]');
2346
+ for (var j = 0; j < closeBtns.length; j++) {
2347
+ wireClose(closeBtns[j]);
2348
+ }
2349
+
2350
+ var overlays = doc.querySelectorAll('.flm-panel-overlay');
2351
+ for (var k = 0; k < overlays.length; k++) {
2352
+ wireOverlay(overlays[k]);
2353
+ }
2354
+ }
2355
+
2356
+ function wireTrigger(btn) {
2357
+ if (btn.getAttribute('data-panel-wired')) return;
2358
+ btn.addEventListener('click', function () {
2359
+ open(btn.getAttribute('data-panel-open'));
2360
+ });
2361
+ btn.setAttribute('data-panel-wired', 'true');
2362
+ }
2363
+
2364
+ function wireClose(btn) {
2365
+ if (btn.getAttribute('data-panel-wired')) return;
2366
+ btn.addEventListener('click', function () {
2367
+ var panel = btn.closest('.flm-panel');
2368
+ if (panel) closePanel(panel);
2369
+ });
2370
+ btn.setAttribute('data-panel-wired', 'true');
2371
+ }
2372
+
2373
+ function wireOverlay(overlay) {
2374
+ if (overlay.getAttribute('data-panel-overlay-wired')) return;
2375
+ overlay.addEventListener('click', function (e) {
2376
+ if (e.target === overlay) {
2377
+ var panel = overlay.nextElementSibling;
2378
+ if (panel && panel.classList.contains('flm-panel')) {
2379
+ closePanel(panel);
2380
+ }
2381
+ }
2382
+ });
2383
+ overlay.setAttribute('data-panel-overlay-wired', 'true');
2384
+ }
2385
+
2386
+ function open(id) {
2387
+ var panel = document.getElementById(id);
2388
+ if (!panel) return;
2389
+
2390
+ // Show overlay
2391
+ var overlay = panel.previousElementSibling;
2392
+ if (overlay && overlay.classList.contains('flm-panel-overlay')) {
2393
+ overlay.classList.add('flm-panel-overlay--open');
2394
+ }
2395
+
2396
+ panel.classList.add('flm-panel--open');
2397
+ document.body.style.overflow = 'hidden';
2398
+
2399
+ var escHandler = function (e) {
2400
+ if (e.key === 'Escape') {
2401
+ closePanel(panel);
2402
+ document.removeEventListener('keydown', escHandler);
2403
+ }
2404
+ };
2405
+ document.addEventListener('keydown', escHandler);
2406
+ panel._escHandler = escHandler;
2407
+
2408
+ setTimeout(function () {
2409
+ var focusable = panel.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
2410
+ if (focusable) focusable.focus();
2411
+ }, 50);
2412
+ }
2413
+
2414
+ function close(id) {
2415
+ var panel = document.getElementById(id);
2416
+ if (panel) closePanel(panel);
2417
+ }
2418
+
2419
+ function closePanel(panel) {
2420
+ panel.classList.remove('flm-panel--open');
2421
+
2422
+ var overlay = panel.previousElementSibling;
2423
+ if (overlay && overlay.classList.contains('flm-panel-overlay')) {
2424
+ overlay.classList.remove('flm-panel-overlay--open');
2425
+ }
2426
+
2427
+ document.body.style.overflow = '';
2428
+ if (panel._escHandler) {
2429
+ document.removeEventListener('keydown', panel._escHandler);
2430
+ delete panel._escHandler;
2431
+ }
2432
+ }
2433
+
2434
+ return { init: init, open: open, close: close };
2435
+ })();
2436
+
2437
+ /**
2438
+ * Pivot component JS — tab switching.
2439
+ * Tabs reference panels by data-panel attribute.
2440
+ */
2441
+ var FluentLMPivotComponent = (function () {
2442
+ 'use strict';
2443
+
2444
+ function init(root) {
2445
+ var doc = root || document;
2446
+
2447
+ var pivots = doc.querySelectorAll('.flm-pivot');
2448
+ for (var i = 0; i < pivots.length; i++) {
2449
+ wirePivot(pivots[i]);
2450
+ }
2451
+ }
2452
+
2453
+ function wirePivot(pivot) {
2454
+ if (pivot.getAttribute('data-pivot-wired')) return;
2455
+
2456
+ var tabs = pivot.querySelectorAll('.flm-pivot-tab');
2457
+ for (var i = 0; i < tabs.length; i++) {
2458
+ wireTab(pivot, tabs[i]);
2459
+ }
2460
+
2461
+ pivot.setAttribute('data-pivot-wired', 'true');
2462
+ }
2463
+
2464
+ function wireTab(pivot, tab) {
2465
+ tab.addEventListener('click', function () {
2466
+ if (tab.classList.contains('flm-pivot-tab--disabled')) return;
2467
+
2468
+ // Deactivate all tabs
2469
+ var allTabs = pivot.querySelectorAll('.flm-pivot-tab');
2470
+ for (var i = 0; i < allTabs.length; i++) {
2471
+ allTabs[i].classList.remove('flm-pivot-tab--active');
2472
+ allTabs[i].setAttribute('aria-selected', 'false');
2473
+ }
2474
+
2475
+ // Hide all panels
2476
+ var allPanels = pivot.querySelectorAll('.flm-pivot-panel');
2477
+ for (var j = 0; j < allPanels.length; j++) {
2478
+ allPanels[j].classList.remove('flm-pivot-panel--active');
2479
+ }
2480
+
2481
+ // Activate clicked tab
2482
+ tab.classList.add('flm-pivot-tab--active');
2483
+ tab.setAttribute('aria-selected', 'true');
2484
+
2485
+ // Show matching panel
2486
+ var panelId = tab.getAttribute('data-panel');
2487
+ if (panelId) {
2488
+ var panel = document.getElementById(panelId);
2489
+ if (panel) panel.classList.add('flm-pivot-panel--active');
2490
+ }
2491
+ });
2492
+ }
2493
+
2494
+ return { init: init };
2495
+ })();
2496
+
2497
+ /**
2498
+ * Rating component JS — stores selected rating value on the root element.
2499
+ */
2500
+ var FluentLMRatingComponent = (function () {
2501
+ 'use strict';
2502
+
2503
+ function init(root) {
2504
+ var doc = root || document;
2505
+
2506
+ var ratings = doc.querySelectorAll('.flm-rating');
2507
+ for (var i = 0; i < ratings.length; i++) {
2508
+ wireRating(ratings[i]);
2509
+ }
2510
+ }
2511
+
2512
+ function wireRating(rating) {
2513
+ if (rating.getAttribute('data-rating-wired')) return;
2514
+
2515
+ var inputs = rating.querySelectorAll('.flm-rating-input');
2516
+ for (var i = 0; i < inputs.length; i++) {
2517
+ inputs[i].addEventListener('change', function () {
2518
+ if (this.checked) {
2519
+ rating.setAttribute('data-rating-value', this.value);
2520
+ }
2521
+ });
2522
+ }
2523
+
2524
+ // Set initial value from pre-checked input
2525
+ var checked = rating.querySelector('.flm-rating-input:checked');
2526
+ if (checked) {
2527
+ rating.setAttribute('data-rating-value', checked.value);
2528
+ }
2529
+
2530
+ rating.setAttribute('data-rating-wired', 'true');
2531
+ }
2532
+
2533
+ return { init: init };
2534
+ })();
2535
+
2536
+ /**
2537
+ * SearchBox component JS — injects search icon, wires clear button.
2538
+ */
2539
+ var FluentLMSearchBoxComponent = (function () {
2540
+ 'use strict';
2541
+
2542
+ function init(root) {
2543
+ var doc = root || document;
2544
+
2545
+ var boxes = doc.querySelectorAll('.flm-searchbox');
2546
+ for (var i = 0; i < boxes.length; i++) {
2547
+ render(boxes[i]);
2548
+ }
2549
+ }
2550
+
2551
+ function render(box) {
2552
+ if (box.getAttribute('data-searchbox-rendered')) return;
2553
+
2554
+ // Inject search icon if not present
2555
+ var iconEl = box.querySelector('.flm-searchbox-icon');
2556
+ if (!iconEl) {
2557
+ iconEl = document.createElement('span');
2558
+ iconEl.className = 'flm-searchbox-icon';
2559
+ var svg = FluentIcons.getSvg('Search');
2560
+ if (svg) iconEl.appendChild(svg);
2561
+ box.insertBefore(iconEl, box.firstChild);
2562
+ }
2563
+
2564
+ // Inject clear button if not present
2565
+ var clearBtn = box.querySelector('.flm-searchbox-clear');
2566
+ if (!clearBtn) {
2567
+ clearBtn = document.createElement('button');
2568
+ clearBtn.className = 'flm-searchbox-clear';
2569
+ clearBtn.setAttribute('aria-label', 'Clear search');
2570
+ clearBtn.type = 'button';
2571
+ var cancelSvg = FluentIcons.getSvg('Cancel');
2572
+ if (cancelSvg) clearBtn.appendChild(cancelSvg);
2573
+ box.appendChild(clearBtn);
2574
+ }
2575
+
2576
+ var input = box.querySelector('.flm-searchbox-input');
2577
+ if (!input) { box.setAttribute('data-searchbox-rendered', 'true'); return; }
2578
+
2579
+ // Toggle has-value class
2580
+ function updateHasValue() {
2581
+ if (input.value) {
2582
+ box.classList.add('flm-searchbox--has-value');
2583
+ } else {
2584
+ box.classList.remove('flm-searchbox--has-value');
2585
+ }
2586
+ }
2587
+
2588
+ input.addEventListener('input', updateHasValue);
2589
+ updateHasValue();
2590
+
2591
+ // Clear button click
2592
+ clearBtn.addEventListener('click', function () {
2593
+ input.value = '';
2594
+ updateHasValue();
2595
+ input.focus();
2596
+ // Fire input event so any listeners are notified
2597
+ input.dispatchEvent(new Event('input', { bubbles: true }));
2598
+ });
2599
+
2600
+ box.setAttribute('data-searchbox-rendered', 'true');
2601
+ }
2602
+
2603
+ return { init: init };
2604
+ })();
2605
+
2606
+ /**
2607
+ * Slider component JS — updates CSS variable for track fill and value display.
2608
+ */
2609
+ var FluentLMSliderComponent = (function () {
2610
+ 'use strict';
2611
+
2612
+ function init(root) {
2613
+ var doc = root || document;
2614
+
2615
+ var sliders = doc.querySelectorAll('.flm-slider');
2616
+ for (var i = 0; i < sliders.length; i++) {
2617
+ wireSlider(sliders[i]);
2618
+ }
2619
+ }
2620
+
2621
+ function wireSlider(el) {
2622
+ if (el.getAttribute('data-slider-wired')) return;
2623
+
2624
+ var input = el.querySelector('.flm-slider-input');
2625
+ if (!input) return;
2626
+
2627
+ var valueDisplay = el.querySelector('.flm-slider-value');
2628
+
2629
+ function update() {
2630
+ var min = parseFloat(input.min) || 0;
2631
+ var max = parseFloat(input.max) || 100;
2632
+ var val = parseFloat(input.value) || 0;
2633
+ var pct = ((val - min) / (max - min)) * 100;
2634
+ input.style.setProperty('--flm-slider-fill', pct + '%');
2635
+
2636
+ if (valueDisplay) {
2637
+ valueDisplay.textContent = input.value;
2638
+ }
2639
+ }
2640
+
2641
+ input.addEventListener('input', update);
2642
+ input.addEventListener('change', update);
2643
+
2644
+ // Set initial fill
2645
+ update();
2646
+
2647
+ el.setAttribute('data-slider-wired', 'true');
2648
+ }
2649
+
2650
+ return { init: init };
2651
+ })();
2652
+
2653
+ /**
2654
+ * SpinButton component JS — wires increment/decrement buttons to numeric input.
2655
+ */
2656
+ var FluentLMSpinButtonComponent = (function () {
2657
+ 'use strict';
2658
+
2659
+ function init(root) {
2660
+ var doc = root || document;
2661
+
2662
+ var spinButtons = doc.querySelectorAll('.flm-spinbutton');
2663
+ for (var i = 0; i < spinButtons.length; i++) {
2664
+ wireSpinButton(spinButtons[i]);
2665
+ }
2666
+ }
2667
+
2668
+ function wireSpinButton(el) {
2669
+ if (el.getAttribute('data-spinbutton-wired')) return;
2670
+
2671
+ var input = el.querySelector('.flm-spinbutton-input');
2672
+ if (!input) return;
2673
+
2674
+ var decBtn = el.querySelector('.flm-spinbutton-btn--decrement');
2675
+ var incBtn = el.querySelector('.flm-spinbutton-btn--increment');
2676
+
2677
+ // Inject icons if empty
2678
+ if (decBtn && !decBtn.innerHTML.trim()) {
2679
+ decBtn.setAttribute('data-icon', 'ChevronDown');
2680
+ }
2681
+ if (incBtn && !incBtn.innerHTML.trim()) {
2682
+ incBtn.setAttribute('data-icon', 'ChevronUp');
2683
+ }
2684
+
2685
+ if (decBtn) {
2686
+ decBtn.addEventListener('click', function () {
2687
+ if (input.disabled) return;
2688
+ try { input.stepDown(); } catch (e) {
2689
+ input.value = (parseFloat(input.value) || 0) - (parseFloat(input.step) || 1);
2690
+ }
2691
+ fireChange(input);
2692
+ });
2693
+ }
2694
+
2695
+ if (incBtn) {
2696
+ incBtn.addEventListener('click', function () {
2697
+ if (input.disabled) return;
2698
+ try { input.stepUp(); } catch (e) {
2699
+ input.value = (parseFloat(input.value) || 0) + (parseFloat(input.step) || 1);
2700
+ }
2701
+ fireChange(input);
2702
+ });
2703
+ }
2704
+
2705
+ el.setAttribute('data-spinbutton-wired', 'true');
2706
+ }
2707
+
2708
+ function fireChange(input) {
2709
+ var evt = document.createEvent('Event');
2710
+ evt.initEvent('change', true, true);
2711
+ input.dispatchEvent(evt);
2712
+ }
2713
+
2714
+ return { init: init };
2715
+ })();
2716
+
2717
+ /**
2718
+ * SwatchColorPicker component JS — manages selection state on color swatches.
2719
+ */
2720
+ var FluentLMSwatchColorPickerComponent = (function () {
2721
+ 'use strict';
2722
+
2723
+ function init(root) {
2724
+ var doc = root || document;
2725
+
2726
+ var pickers = doc.querySelectorAll('.flm-swatchcolorpicker');
2727
+ for (var i = 0; i < pickers.length; i++) {
2728
+ wirePicker(pickers[i]);
2729
+ }
2730
+ }
2731
+
2732
+ function wirePicker(picker) {
2733
+ if (picker.getAttribute('data-swatchcolorpicker-wired')) return;
2734
+
2735
+ var cells = picker.querySelectorAll('.flm-swatchcolorpicker-cell');
2736
+ for (var i = 0; i < cells.length; i++) {
2737
+ wireCell(picker, cells[i]);
2738
+ }
2739
+
2740
+ picker.setAttribute('data-swatchcolorpicker-wired', 'true');
2741
+ }
2742
+
2743
+ function wireCell(picker, cell) {
2744
+ cell.addEventListener('click', function () {
2745
+ if (cell.disabled || cell.classList.contains('flm-swatchcolorpicker-cell--disabled')) return;
2746
+
2747
+ // Clear previous selection
2748
+ var prev = picker.querySelector('.flm-swatchcolorpicker-cell--selected');
2749
+ if (prev) {
2750
+ prev.classList.remove('flm-swatchcolorpicker-cell--selected');
2751
+ }
2752
+
2753
+ // Select this cell
2754
+ cell.classList.add('flm-swatchcolorpicker-cell--selected');
2755
+
2756
+ // Store selected color on root
2757
+ var color = cell.getAttribute('data-color') || cell.style.backgroundColor || '';
2758
+ picker.setAttribute('data-selected', 'true');
2759
+ picker.setAttribute('data-selected-color', color);
2760
+ });
2761
+ }
2762
+
2763
+ return { init: init };
2764
+ })();
2765
+
2766
+ /**
2767
+ * TagPicker / PeoplePicker component JS — multi-select input with chip/tag UI.
2768
+ *
2769
+ * Usage:
2770
+ * <div class="flm-tagpicker">
2771
+ * <label class="flm-label">Tags</label>
2772
+ * <div class="flm-tagpicker-well">
2773
+ * <input class="flm-tagpicker-input" placeholder="Add tags…">
2774
+ * </div>
2775
+ * <div class="flm-tagpicker-listbox">
2776
+ * <div class="flm-tagpicker-option" data-value="a">Alpha</div>
2777
+ * <div class="flm-tagpicker-option" data-value="b">Beta</div>
2778
+ * </div>
2779
+ * </div>
2780
+ *
2781
+ * PeoplePicker variant: add flm-tagpicker--people on root, use
2782
+ * data-initials="JD" and data-secondary="Engineer" on options.
2783
+ *
2784
+ * Attributes:
2785
+ * data-max-tags="N" — limits number of selected tags.
2786
+ * data-selected-values — CSV of selected values (auto-maintained).
2787
+ */
2788
+ var FluentLMTagPickerComponent = (function () {
2789
+ 'use strict';
2790
+
2791
+ function init(root) {
2792
+ var doc = root || document;
2793
+ var pickers = doc.querySelectorAll('.flm-tagpicker');
2794
+ for (var i = 0; i < pickers.length; i++) {
2795
+ wirePicker(pickers[i]);
2796
+ }
2797
+ }
2798
+
2799
+ function wirePicker(el) {
2800
+ if (el.getAttribute('data-tagpicker-wired')) return;
2801
+ if (el.classList.contains('flm-tagpicker--disabled')) return;
2802
+
2803
+ var well = el.querySelector('.flm-tagpicker-well');
2804
+ var input = el.querySelector('.flm-tagpicker-input');
2805
+ var listbox = el.querySelector('.flm-tagpicker-listbox');
2806
+
2807
+ if (!well || !input || !listbox) return;
2808
+
2809
+ var isPeople = el.classList.contains('flm-tagpicker--people');
2810
+ var highlighted = -1;
2811
+
2812
+ function getVisibleOptions() {
2813
+ return listbox.querySelectorAll('.flm-tagpicker-option:not(.flm-tagpicker-option--selected):not(.flm-tagpicker-option--hidden)');
2814
+ }
2815
+
2816
+ function isOpen() {
2817
+ return listbox.classList.contains('flm-tagpicker-listbox--open');
2818
+ }
2819
+
2820
+ function open() {
2821
+ listbox.classList.add('flm-tagpicker-listbox--open');
2822
+ highlighted = -1;
2823
+ flipIfNeeded();
2824
+ }
2825
+
2826
+ function close() {
2827
+ listbox.classList.remove('flm-tagpicker-listbox--open', 'flm-tagpicker-listbox--above');
2828
+ highlighted = -1;
2829
+ clearHighlight();
2830
+ }
2831
+
2832
+ function flipIfNeeded() {
2833
+ setTimeout(function () {
2834
+ var rect = listbox.getBoundingClientRect();
2835
+ if (rect.bottom > window.innerHeight) {
2836
+ listbox.classList.add('flm-tagpicker-listbox--above');
2837
+ } else {
2838
+ listbox.classList.remove('flm-tagpicker-listbox--above');
2839
+ }
2840
+ }, 0);
2841
+ }
2842
+
2843
+ function clearHighlight() {
2844
+ var opts = listbox.querySelectorAll('.flm-tagpicker-option--highlighted');
2845
+ for (var i = 0; i < opts.length; i++) {
2846
+ opts[i].classList.remove('flm-tagpicker-option--highlighted');
2847
+ }
2848
+ }
2849
+
2850
+ function setHighlight(idx) {
2851
+ clearHighlight();
2852
+ var opts = getVisibleOptions();
2853
+ if (idx >= 0 && idx < opts.length) {
2854
+ highlighted = idx;
2855
+ opts[idx].classList.add('flm-tagpicker-option--highlighted');
2856
+ opts[idx].scrollIntoView({ block: 'nearest' });
2857
+ }
2858
+ }
2859
+
2860
+ function filterOptions() {
2861
+ var text = input.value.toLowerCase();
2862
+ var allOpts = listbox.querySelectorAll('.flm-tagpicker-option');
2863
+ for (var i = 0; i < allOpts.length; i++) {
2864
+ var optText = allOpts[i].textContent.toLowerCase();
2865
+ if (text === '' || optText.indexOf(text) !== -1) {
2866
+ allOpts[i].classList.remove('flm-tagpicker-option--hidden');
2867
+ } else {
2868
+ allOpts[i].classList.add('flm-tagpicker-option--hidden');
2869
+ }
2870
+ }
2871
+ highlighted = -1;
2872
+ }
2873
+
2874
+ function getMaxTags() {
2875
+ var max = el.getAttribute('data-max-tags');
2876
+ return max ? parseInt(max, 10) : 0;
2877
+ }
2878
+
2879
+ function getChips() {
2880
+ return well.querySelectorAll('.flm-tagpicker-chip');
2881
+ }
2882
+
2883
+ function updateSelectedValues() {
2884
+ var chips = getChips();
2885
+ var values = [];
2886
+ for (var i = 0; i < chips.length; i++) {
2887
+ values.push(chips[i].getAttribute('data-value'));
2888
+ }
2889
+ el.setAttribute('data-selected-values', values.join(','));
2890
+
2891
+ // Fire change event
2892
+ var evt = document.createEvent('Event');
2893
+ evt.initEvent('change', true, true);
2894
+ el.dispatchEvent(evt);
2895
+ }
2896
+
2897
+ function addChip(opt) {
2898
+ var max = getMaxTags();
2899
+ if (max > 0 && getChips().length >= max) return;
2900
+
2901
+ var value = opt.getAttribute('data-value') || opt.textContent.trim();
2902
+ var text = opt.getAttribute('data-value') ? opt.textContent.trim() : value;
2903
+
2904
+ // For people picker, try to get just the name
2905
+ var nameEl = opt.querySelector('.flm-tagpicker-option-name');
2906
+ if (nameEl) {
2907
+ text = nameEl.textContent.trim();
2908
+ }
2909
+
2910
+ var chip = document.createElement('span');
2911
+ chip.className = 'flm-tagpicker-chip';
2912
+ chip.setAttribute('data-value', value);
2913
+
2914
+ // People variant: add small coin
2915
+ if (isPeople) {
2916
+ var initials = opt.getAttribute('data-initials') || '';
2917
+ if (initials) {
2918
+ var coin = document.createElement('span');
2919
+ coin.className = 'flm-tagpicker-chip-coin';
2920
+ coin.textContent = initials;
2921
+ chip.appendChild(coin);
2922
+ }
2923
+ }
2924
+
2925
+ var textSpan = document.createElement('span');
2926
+ textSpan.className = 'flm-tagpicker-chip-text';
2927
+ textSpan.textContent = text;
2928
+ chip.appendChild(textSpan);
2929
+
2930
+ var removeBtn = document.createElement('button');
2931
+ removeBtn.className = 'flm-tagpicker-chip-remove';
2932
+ removeBtn.setAttribute('aria-label', 'Remove ' + text);
2933
+ removeBtn.type = 'button';
2934
+ removeBtn.innerHTML = '<svg viewBox="0 0 8 8" xmlns="http://www.w3.org/2000/svg"><path d="M1.17.46L4 3.3 6.83.46a.5.5 0 0 1 .71.71L4.7 4l2.84 2.83a.5.5 0 0 1-.71.71L4 4.7 1.17 7.54a.5.5 0 0 1-.71-.71L3.3 4 .46 1.17A.5.5 0 0 1 1.17.46z"/></svg>';
2935
+ chip.appendChild(removeBtn);
2936
+
2937
+ // Insert chip before input
2938
+ well.insertBefore(chip, input);
2939
+
2940
+ // Mark option as selected
2941
+ opt.classList.add('flm-tagpicker-option--selected');
2942
+
2943
+ // Wire remove button
2944
+ removeBtn.addEventListener('click', function (e) {
2945
+ e.stopPropagation();
2946
+ removeChip(chip, opt);
2947
+ });
2948
+
2949
+ input.value = '';
2950
+ filterOptions();
2951
+ updateSelectedValues();
2952
+ }
2953
+
2954
+ function removeChip(chip, opt) {
2955
+ chip.parentNode.removeChild(chip);
2956
+ opt.classList.remove('flm-tagpicker-option--selected');
2957
+ updateSelectedValues();
2958
+ input.focus();
2959
+ }
2960
+
2961
+ // Click on well focuses input
2962
+ well.addEventListener('click', function () {
2963
+ input.focus();
2964
+ });
2965
+
2966
+ // Input events
2967
+ input.addEventListener('focus', function () {
2968
+ if (!isOpen()) {
2969
+ filterOptions();
2970
+ open();
2971
+ }
2972
+ });
2973
+
2974
+ input.addEventListener('input', function () {
2975
+ if (!isOpen()) open();
2976
+ filterOptions();
2977
+ });
2978
+
2979
+ // Keyboard navigation
2980
+ input.addEventListener('keydown', function (e) {
2981
+ var opts = getVisibleOptions();
2982
+ var len = opts.length;
2983
+
2984
+ if (e.key === 'ArrowDown' || e.keyCode === 40) {
2985
+ e.preventDefault();
2986
+ if (!isOpen()) { filterOptions(); open(); }
2987
+ setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
2988
+ } else if (e.key === 'ArrowUp' || e.keyCode === 38) {
2989
+ e.preventDefault();
2990
+ if (!isOpen()) { filterOptions(); open(); }
2991
+ setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
2992
+ } else if (e.key === 'Enter' || e.keyCode === 13) {
2993
+ e.preventDefault();
2994
+ if (highlighted >= 0 && highlighted < len) {
2995
+ addChip(opts[highlighted]);
2996
+ highlighted = -1;
2997
+ }
2998
+ } else if (e.key === 'Escape' || e.keyCode === 27) {
2999
+ close();
3000
+ } else if ((e.key === 'Backspace' || e.keyCode === 8) && input.value === '') {
3001
+ // Remove last chip on backspace with empty input
3002
+ var chips = getChips();
3003
+ if (chips.length > 0) {
3004
+ var lastChip = chips[chips.length - 1];
3005
+ var chipValue = lastChip.getAttribute('data-value');
3006
+ // Find corresponding option
3007
+ var allOpts = listbox.querySelectorAll('.flm-tagpicker-option');
3008
+ for (var i = 0; i < allOpts.length; i++) {
3009
+ var optVal = allOpts[i].getAttribute('data-value') || allOpts[i].textContent.trim();
3010
+ if (optVal === chipValue) {
3011
+ removeChip(lastChip, allOpts[i]);
3012
+ break;
3013
+ }
3014
+ }
3015
+ }
3016
+ }
3017
+ });
3018
+
3019
+ // Option click
3020
+ listbox.addEventListener('click', function (e) {
3021
+ var opt = e.target.closest('.flm-tagpicker-option');
3022
+ if (opt && !opt.classList.contains('flm-tagpicker-option--selected')) {
3023
+ addChip(opt);
3024
+ input.focus();
3025
+ }
3026
+ });
3027
+
3028
+ // Click outside
3029
+ document.addEventListener('click', function (e) {
3030
+ if (!el.contains(e.target) && isOpen()) {
3031
+ close();
3032
+ }
3033
+ });
3034
+
3035
+ el.setAttribute('data-tagpicker-wired', 'true');
3036
+ }
3037
+
3038
+ return { init: init };
3039
+ })();
3040
+
3041
+ /**
3042
+ * TeachingBubble component JS — positioned inverted callout.
3043
+ *
3044
+ * Usage:
3045
+ * <button data-teachingbubble-toggle="tb1">Learn more</button>
3046
+ * <div class="flm-teachingbubble" id="tb1">
3047
+ * <div class="flm-teachingbubble-beak"></div>
3048
+ * <div class="flm-teachingbubble-header">
3049
+ * <h3 class="flm-teachingbubble-headline">Title</h3>
3050
+ * <button class="flm-teachingbubble-close" data-icon="Cancel" aria-label="Close"></button>
3051
+ * </div>
3052
+ * <div class="flm-teachingbubble-body">Body text</div>
3053
+ * <div class="flm-teachingbubble-footer">
3054
+ * <button class="flm-button">Got it</button>
3055
+ * </div>
3056
+ * </div>
3057
+ */
3058
+ var FluentLMTeachingBubbleComponent = (function () {
3059
+ 'use strict';
3060
+
3061
+ function init(root) {
3062
+ var doc = root || document;
3063
+
3064
+ var triggers = doc.querySelectorAll('[data-teachingbubble-toggle]');
3065
+ for (var i = 0; i < triggers.length; i++) {
3066
+ // Skip coachmarks — they wire their own click handler
3067
+ if (triggers[i].classList.contains('flm-coachmark')) continue;
3068
+ wireTrigger(triggers[i]);
3069
+ }
3070
+
3071
+ // Wire close buttons
3072
+ var closeBtns = doc.querySelectorAll('.flm-teachingbubble-close');
3073
+ for (var j = 0; j < closeBtns.length; j++) {
3074
+ wireClose(closeBtns[j]);
3075
+ }
3076
+
3077
+ // Wire footer buttons that should dismiss
3078
+ var footerBtns = doc.querySelectorAll('.flm-teachingbubble-footer .flm-button');
3079
+ for (var k = 0; k < footerBtns.length; k++) {
3080
+ wireFooterBtn(footerBtns[k]);
3081
+ }
3082
+ }
3083
+
3084
+ function wireTrigger(btn) {
3085
+ if (btn.getAttribute('data-teachingbubble-wired')) return;
3086
+
3087
+ btn.addEventListener('click', function (e) {
3088
+ var id = btn.getAttribute('data-teachingbubble-toggle');
3089
+ var bubble = document.getElementById(id);
3090
+ if (!bubble) return;
3091
+
3092
+ if (bubble.classList.contains('flm-teachingbubble--visible')) {
3093
+ hide(bubble);
3094
+ } else {
3095
+ show(bubble, btn);
3096
+ }
3097
+ e.stopPropagation();
3098
+ });
3099
+
3100
+ btn.setAttribute('data-teachingbubble-wired', 'true');
3101
+ }
3102
+
3103
+ function wireClose(btn) {
3104
+ if (btn.getAttribute('data-teachingbubble-close-wired')) return;
3105
+
3106
+ btn.addEventListener('click', function () {
3107
+ var bubble = btn.closest('.flm-teachingbubble');
3108
+ if (bubble) hide(bubble);
3109
+ });
3110
+
3111
+ btn.setAttribute('data-teachingbubble-close-wired', 'true');
3112
+ }
3113
+
3114
+ function wireFooterBtn(btn) {
3115
+ if (btn.getAttribute('data-teachingbubble-footer-wired')) return;
3116
+
3117
+ btn.addEventListener('click', function () {
3118
+ var bubble = btn.closest('.flm-teachingbubble');
3119
+ if (bubble) hide(bubble);
3120
+ });
3121
+
3122
+ btn.setAttribute('data-teachingbubble-footer-wired', 'true');
3123
+ }
3124
+
3125
+ function show(bubble, target) {
3126
+ var rect = target.getBoundingClientRect();
3127
+ var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
3128
+ var scrollY = window.pageYOffset || document.documentElement.scrollTop;
3129
+
3130
+ // Account for positioned offset parent so absolute coords are correct
3131
+ var offsetX = 0;
3132
+ var offsetY = 0;
3133
+ var offsetParent = bubble.offsetParent;
3134
+ if (offsetParent && offsetParent !== document.body && offsetParent !== document.documentElement) {
3135
+ var parentRect = offsetParent.getBoundingClientRect();
3136
+ offsetX = parentRect.left + scrollX;
3137
+ offsetY = parentRect.top + scrollY;
3138
+ }
3139
+
3140
+ bubble.style.position = 'absolute';
3141
+ bubble.style.left = (rect.left + scrollX - offsetX) + 'px';
3142
+ bubble.style.top = (rect.bottom + scrollY - offsetY + 8) + 'px';
3143
+
3144
+ bubble.classList.add('flm-teachingbubble--visible');
3145
+ bubble.classList.remove('flm-teachingbubble--above');
3146
+
3147
+ // Position beak to point at target center
3148
+ setTimeout(function () {
3149
+ var bRect = bubble.getBoundingClientRect();
3150
+ var beak = bubble.querySelector('.flm-teachingbubble-beak');
3151
+ if (beak) {
3152
+ var targetCenterX = rect.left + rect.width / 2;
3153
+ var beakLeft = targetCenterX - bRect.left - 8; // 8 = half of 16px beak
3154
+ beakLeft = Math.max(8, Math.min(beakLeft, bRect.width - 24));
3155
+ beak.style.left = beakLeft + 'px';
3156
+ }
3157
+
3158
+ // Flip above if off-screen
3159
+ if (bRect.bottom > window.innerHeight) {
3160
+ bubble.classList.add('flm-teachingbubble--above');
3161
+ bubble.style.top = (rect.top + scrollY - offsetY - bRect.height - 8) + 'px';
3162
+ }
3163
+ }, 0);
3164
+
3165
+ // Click outside to dismiss
3166
+ var outsideHandler = function (e) {
3167
+ if (!bubble.contains(e.target) && !target.contains(e.target)) {
3168
+ hide(bubble);
3169
+ document.removeEventListener('click', outsideHandler);
3170
+ }
3171
+ };
3172
+ setTimeout(function () {
3173
+ document.addEventListener('click', outsideHandler);
3174
+ }, 0);
3175
+ bubble._outsideHandler = outsideHandler;
3176
+ }
3177
+
3178
+ function hide(bubble) {
3179
+ bubble.classList.remove('flm-teachingbubble--visible', 'flm-teachingbubble--above');
3180
+ if (bubble._outsideHandler) {
3181
+ document.removeEventListener('click', bubble._outsideHandler);
3182
+ delete bubble._outsideHandler;
3183
+ }
3184
+ }
3185
+
3186
+ return { init: init, show: show, hide: hide };
3187
+ })();
3188
+
3189
+ /**
3190
+ * TimePicker component JS — scrollable time-slot dropdown with filtering.
3191
+ *
3192
+ * Usage:
3193
+ * <div class="flm-timepicker" data-increment="30" data-use-12h data-min-time="09:00" data-max-time="17:00">
3194
+ * <label class="flm-label" for="tp1">Time</label>
3195
+ * <div class="flm-timepicker-wrapper">
3196
+ * <input class="flm-timepicker-input" id="tp1" placeholder="Select a time…">
3197
+ * <button class="flm-timepicker-icon" data-icon="Clock" aria-label="Open time picker"></button>
3198
+ * </div>
3199
+ * </div>
3200
+ *
3201
+ * Attributes on .flm-timepicker:
3202
+ * data-increment="30" — minute increment (default 30)
3203
+ * data-use-12h — 12-hour format with AM/PM (default 24h)
3204
+ * data-min-time="09:00" — earliest available time (HH:MM, 24h)
3205
+ * data-max-time="17:00" — latest available time (HH:MM, 24h)
3206
+ */
3207
+ var FluentLMTimePickerComponent = (function () {
3208
+ 'use strict';
3209
+
3210
+ function init(root) {
3211
+ var doc = root || document;
3212
+ var pickers = doc.querySelectorAll('.flm-timepicker');
3213
+ for (var i = 0; i < pickers.length; i++) {
3214
+ wirePicker(pickers[i]);
3215
+ }
3216
+ }
3217
+
3218
+ function wirePicker(el) {
3219
+ if (el.getAttribute('data-timepicker-wired')) return;
3220
+
3221
+ var input = el.querySelector('.flm-timepicker-input');
3222
+ var iconBtn = el.querySelector('.flm-timepicker-icon');
3223
+
3224
+ if (!input) return;
3225
+
3226
+ // Configuration
3227
+ var increment = parseInt(el.getAttribute('data-increment'), 10) || 30;
3228
+ var use12h = el.hasAttribute('data-use-12h');
3229
+ var minTime = parseTime(el.getAttribute('data-min-time'));
3230
+ var maxTime = parseTime(el.getAttribute('data-max-time'));
3231
+
3232
+ var highlighted = -1;
3233
+
3234
+ // Create listbox
3235
+ var listbox = document.createElement('div');
3236
+ listbox.className = 'flm-timepicker-listbox';
3237
+ el.appendChild(listbox);
3238
+
3239
+ // Generate options
3240
+ generateOptions();
3241
+
3242
+ function parseTime(str) {
3243
+ if (!str) return null;
3244
+ var parts = str.split(':');
3245
+ return parseInt(parts[0], 10) * 60 + parseInt(parts[1], 10);
3246
+ }
3247
+
3248
+ function padTwo(n) {
3249
+ return n < 10 ? '0' + n : '' + n;
3250
+ }
3251
+
3252
+ function formatTime(totalMinutes) {
3253
+ var h = Math.floor(totalMinutes / 60) % 24;
3254
+ var m = totalMinutes % 60;
3255
+ if (use12h) {
3256
+ var period = h < 12 ? 'AM' : 'PM';
3257
+ var h12 = h % 12;
3258
+ if (h12 === 0) h12 = 12;
3259
+ return h12 + ':' + padTwo(m) + ' ' + period;
3260
+ }
3261
+ return padTwo(h) + ':' + padTwo(m);
3262
+ }
3263
+
3264
+ function generateOptions() {
3265
+ listbox.innerHTML = '';
3266
+ for (var t = 0; t < 1440; t += increment) {
3267
+ if (minTime !== null && t < minTime) continue;
3268
+ if (maxTime !== null && t > maxTime) continue;
3269
+ var opt = document.createElement('div');
3270
+ opt.className = 'flm-timepicker-option';
3271
+ opt.setAttribute('data-value', padTwo(Math.floor(t / 60)) + ':' + padTwo(t % 60));
3272
+ opt.textContent = formatTime(t);
3273
+ listbox.appendChild(opt);
3274
+ }
3275
+ }
3276
+
3277
+ function getOptions() {
3278
+ return listbox.querySelectorAll('.flm-timepicker-option:not(.flm-timepicker-option--hidden)');
3279
+ }
3280
+
3281
+ function isOpen() {
3282
+ return listbox.classList.contains('flm-timepicker-listbox--open');
3283
+ }
3284
+
3285
+ function open() {
3286
+ document.dispatchEvent(new CustomEvent('flm-dismiss-pickers', { detail: { source: el } }));
3287
+ listbox.classList.add('flm-timepicker-listbox--open');
3288
+ highlighted = -1;
3289
+ flipIfNeeded();
3290
+ scrollToSelected();
3291
+ }
3292
+
3293
+ function close() {
3294
+ listbox.classList.remove('flm-timepicker-listbox--open', 'flm-timepicker-listbox--above');
3295
+ highlighted = -1;
3296
+ clearHighlight();
3297
+ }
3298
+
3299
+ function flipIfNeeded() {
3300
+ setTimeout(function () {
3301
+ var rect = listbox.getBoundingClientRect();
3302
+ if (rect.bottom > window.innerHeight) {
3303
+ listbox.classList.add('flm-timepicker-listbox--above');
3304
+ } else {
3305
+ listbox.classList.remove('flm-timepicker-listbox--above');
3306
+ }
3307
+ }, 0);
3308
+ }
3309
+
3310
+ function scrollToSelected() {
3311
+ setTimeout(function () {
3312
+ // Scroll to selected option, or nearest-to-current-time option
3313
+ var selected = listbox.querySelector('.flm-timepicker-option--selected');
3314
+ if (selected) {
3315
+ selected.scrollIntoView({ block: 'nearest' });
3316
+ return;
3317
+ }
3318
+ // Find nearest to current time
3319
+ var now = new Date();
3320
+ var nowMinutes = now.getHours() * 60 + now.getMinutes();
3321
+ var opts = getOptions();
3322
+ var bestOpt = null;
3323
+ var bestDiff = Infinity;
3324
+ for (var i = 0; i < opts.length; i++) {
3325
+ var val = parseTime(opts[i].getAttribute('data-value'));
3326
+ var diff = Math.abs(val - nowMinutes);
3327
+ if (diff < bestDiff) {
3328
+ bestDiff = diff;
3329
+ bestOpt = opts[i];
3330
+ }
3331
+ }
3332
+ if (bestOpt) {
3333
+ bestOpt.scrollIntoView({ block: 'nearest' });
3334
+ }
3335
+ }, 0);
3336
+ }
3337
+
3338
+ function clearHighlight() {
3339
+ var opts = listbox.querySelectorAll('.flm-timepicker-option--highlighted');
3340
+ for (var i = 0; i < opts.length; i++) {
3341
+ opts[i].classList.remove('flm-timepicker-option--highlighted');
3342
+ }
3343
+ }
3344
+
3345
+ function setHighlight(idx) {
3346
+ clearHighlight();
3347
+ var opts = getOptions();
3348
+ if (idx >= 0 && idx < opts.length) {
3349
+ highlighted = idx;
3350
+ opts[idx].classList.add('flm-timepicker-option--highlighted');
3351
+ opts[idx].scrollIntoView({ block: 'nearest' });
3352
+ }
3353
+ }
3354
+
3355
+ function filterOptions() {
3356
+ var text = input.value.toLowerCase();
3357
+ var allOpts = listbox.querySelectorAll('.flm-timepicker-option');
3358
+ for (var i = 0; i < allOpts.length; i++) {
3359
+ var optText = allOpts[i].textContent.toLowerCase();
3360
+ if (text === '' || optText.indexOf(text) !== -1) {
3361
+ allOpts[i].classList.remove('flm-timepicker-option--hidden');
3362
+ } else {
3363
+ allOpts[i].classList.add('flm-timepicker-option--hidden');
3364
+ }
3365
+ }
3366
+ highlighted = -1;
3367
+ }
3368
+
3369
+ function selectOption(opt) {
3370
+ var value = opt.getAttribute('data-value');
3371
+ var text = opt.textContent;
3372
+
3373
+ // Clear previous selection
3374
+ var prev = listbox.querySelectorAll('.flm-timepicker-option--selected');
3375
+ for (var j = 0; j < prev.length; j++) {
3376
+ prev[j].classList.remove('flm-timepicker-option--selected');
3377
+ }
3378
+ opt.classList.add('flm-timepicker-option--selected');
3379
+ input.value = text;
3380
+ el.setAttribute('data-value', value);
3381
+ close();
3382
+
3383
+ // Reset filter so all options are visible next time
3384
+ filterOptions();
3385
+
3386
+ // Fire change event
3387
+ var evt = document.createEvent('Event');
3388
+ evt.initEvent('change', true, true);
3389
+ input.dispatchEvent(evt);
3390
+ }
3391
+
3392
+ // Input events
3393
+ input.addEventListener('focus', function () {
3394
+ if (!isOpen()) open();
3395
+ });
3396
+
3397
+ input.addEventListener('input', function () {
3398
+ if (!isOpen()) open();
3399
+ filterOptions();
3400
+ });
3401
+
3402
+ // Icon toggle
3403
+ if (iconBtn) {
3404
+ iconBtn.addEventListener('click', function (e) {
3405
+ e.stopPropagation();
3406
+ if (isOpen()) {
3407
+ close();
3408
+ } else {
3409
+ filterOptions();
3410
+ open();
3411
+ input.focus();
3412
+ }
3413
+ });
3414
+ }
3415
+
3416
+ // Keyboard navigation
3417
+ input.addEventListener('keydown', function (e) {
3418
+ var opts = getOptions();
3419
+ var len = opts.length;
3420
+
3421
+ if (e.key === 'ArrowDown' || e.keyCode === 40) {
3422
+ e.preventDefault();
3423
+ if (!isOpen()) { open(); filterOptions(); }
3424
+ setHighlight(highlighted < len - 1 ? highlighted + 1 : 0);
3425
+ } else if (e.key === 'ArrowUp' || e.keyCode === 38) {
3426
+ e.preventDefault();
3427
+ if (!isOpen()) { open(); filterOptions(); }
3428
+ setHighlight(highlighted > 0 ? highlighted - 1 : len - 1);
3429
+ } else if (e.key === 'Enter' || e.keyCode === 13) {
3430
+ e.preventDefault();
3431
+ if (highlighted >= 0 && highlighted < len) {
3432
+ selectOption(opts[highlighted]);
3433
+ }
3434
+ } else if (e.key === 'Escape' || e.keyCode === 27) {
3435
+ close();
3436
+ }
3437
+ });
3438
+
3439
+ // Option click
3440
+ listbox.addEventListener('click', function (e) {
3441
+ var opt = e.target.closest('.flm-timepicker-option');
3442
+ if (opt) {
3443
+ selectOption(opt);
3444
+ }
3445
+ });
3446
+
3447
+ // Click outside
3448
+ document.addEventListener('click', function (e) {
3449
+ if (!el.contains(e.target) && isOpen()) {
3450
+ close();
3451
+ }
3452
+ });
3453
+
3454
+ // Close when another picker opens
3455
+ document.addEventListener('flm-dismiss-pickers', function (e) {
3456
+ if (e.detail.source !== el && isOpen()) close();
3457
+ });
3458
+
3459
+ el.setAttribute('data-timepicker-wired', 'true');
3460
+ }
3461
+
3462
+ return { init: init };
3463
+ })();
3464
+
3465
+ /**
3466
+ * Toggle component JS — initializes state text from data-on / data-off.
3467
+ * The CSS handles display via content: attr(data-on) / attr(data-off)
3468
+ * using the :checked sibling selector, so this module only needs to
3469
+ * handle any initial ARIA setup.
3470
+ */
3471
+ var FluentLMToggleComponent = (function () {
3472
+ 'use strict';
3473
+
3474
+ function init(root) {
3475
+ var els = (root || document).querySelectorAll('.flm-toggle');
3476
+ for (var i = 0; i < els.length; i++) {
3477
+ render(els[i]);
3478
+ }
3479
+ }
3480
+
3481
+ function render(el) {
3482
+ var input = el.querySelector('.flm-toggle-input');
3483
+ if (!input) return;
3484
+
3485
+ // Set initial ARIA
3486
+ input.setAttribute('role', 'switch');
3487
+ input.setAttribute('aria-checked', input.checked ? 'true' : 'false');
3488
+
3489
+ // Keep aria-checked in sync
3490
+ input.addEventListener('change', function () {
3491
+ input.setAttribute('aria-checked', input.checked ? 'true' : 'false');
3492
+ });
3493
+ }
3494
+
3495
+ return { init: init };
3496
+ })();
3497
+
3498
+ /**
3499
+ * Tooltip component JS — shows tooltip on hover/focus of host elements.
3500
+ *
3501
+ * Usage: <span class="flm-tooltip-host" data-tooltip="Help text">Hover me</span>
3502
+ * Or: <span class="flm-tooltip-host" data-tooltip-id="my-tooltip">Hover me</span>
3503
+ * <div id="my-tooltip" class="flm-tooltip">Rich tooltip content</div>
3504
+ */
3505
+ var FluentLMTooltipComponent = (function () {
3506
+ 'use strict';
3507
+
3508
+ var activeTooltip = null;
3509
+ var showDelay = 300;
3510
+
3511
+ function init(root) {
3512
+ var doc = root || document;
3513
+
3514
+ var hosts = doc.querySelectorAll('.flm-tooltip-host, [data-tooltip]');
3515
+ for (var i = 0; i < hosts.length; i++) {
3516
+ wireHost(hosts[i]);
3517
+ }
3518
+ }
3519
+
3520
+ function wireHost(host) {
3521
+ if (host.getAttribute('data-tooltip-wired')) return;
3522
+
3523
+ var timer = null;
3524
+
3525
+ host.addEventListener('mouseenter', function () {
3526
+ timer = setTimeout(function () { showForHost(host); }, showDelay);
3527
+ });
3528
+
3529
+ host.addEventListener('mouseleave', function () {
3530
+ clearTimeout(timer);
3531
+ hideActive();
3532
+ });
3533
+
3534
+ host.addEventListener('focus', function () {
3535
+ timer = setTimeout(function () { showForHost(host); }, showDelay);
3536
+ });
3537
+
3538
+ host.addEventListener('blur', function () {
3539
+ clearTimeout(timer);
3540
+ hideActive();
3541
+ });
3542
+
3543
+ host.setAttribute('data-tooltip-wired', 'true');
3544
+ }
3545
+
3546
+ function showForHost(host) {
3547
+ hideActive();
3548
+
3549
+ var tooltip;
3550
+ var tooltipId = host.getAttribute('data-tooltip-id');
3551
+
3552
+ if (tooltipId) {
3553
+ tooltip = document.getElementById(tooltipId);
3554
+ } else {
3555
+ // Create a dynamic tooltip from data-tooltip text
3556
+ var text = host.getAttribute('data-tooltip');
3557
+ if (!text) return;
3558
+
3559
+ tooltip = document.createElement('div');
3560
+ tooltip.className = 'flm-tooltip';
3561
+ tooltip.textContent = text;
3562
+ tooltip._dynamic = true;
3563
+ document.body.appendChild(tooltip);
3564
+ }
3565
+
3566
+ if (!tooltip) return;
3567
+
3568
+ // Position below host
3569
+ var rect = host.getBoundingClientRect();
3570
+ var scrollX = window.pageXOffset || document.documentElement.scrollLeft;
3571
+ var scrollY = window.pageYOffset || document.documentElement.scrollTop;
3572
+
3573
+ tooltip.style.position = 'absolute';
3574
+ tooltip.style.left = rect.left + scrollX + 'px';
3575
+ tooltip.style.top = (rect.bottom + scrollY + 4) + 'px';
3576
+ tooltip.classList.add('flm-tooltip--visible');
3577
+
3578
+ activeTooltip = tooltip;
3579
+
3580
+ // Flip if off-screen
3581
+ setTimeout(function () {
3582
+ var tRect = tooltip.getBoundingClientRect();
3583
+ if (tRect.bottom > window.innerHeight) {
3584
+ tooltip.style.top = (rect.top + scrollY - tRect.height - 4) + 'px';
3585
+ }
3586
+ if (tRect.right > window.innerWidth) {
3587
+ tooltip.style.left = (rect.right + scrollX - tRect.width) + 'px';
3588
+ }
3589
+ }, 0);
3590
+ }
3591
+
3592
+ function hideActive() {
3593
+ if (!activeTooltip) return;
3594
+ activeTooltip.classList.remove('flm-tooltip--visible');
3595
+ if (activeTooltip._dynamic) {
3596
+ activeTooltip.parentNode.removeChild(activeTooltip);
3597
+ }
3598
+ activeTooltip = null;
3599
+ }
3600
+
3601
+ return { init: init };
3602
+ })();