@openclaw/line 2026.6.11 → 2026.7.1-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1127 @@
1
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
2
+ //#region extensions/line/src/flex-templates/common.ts
3
+ function attachFooterText(bubble, footer) {
4
+ bubble.footer = {
5
+ type: "box",
6
+ layout: "vertical",
7
+ contents: [{
8
+ type: "text",
9
+ text: footer,
10
+ size: "xs",
11
+ color: "#AAAAAA",
12
+ wrap: true,
13
+ align: "center"
14
+ }],
15
+ paddingAll: "lg",
16
+ backgroundColor: "#FAFAFA"
17
+ };
18
+ }
19
+ //#endregion
20
+ //#region extensions/line/src/flex-templates/basic-cards.ts
21
+ /**
22
+ * Create an info card with title, body, and optional footer
23
+ *
24
+ * Editorial design: Clean hierarchy with accent bar, generous spacing,
25
+ * and subtle background zones for visual separation.
26
+ */
27
+ function createInfoCard(title, body, footer) {
28
+ const bubble = {
29
+ type: "bubble",
30
+ size: "mega",
31
+ body: {
32
+ type: "box",
33
+ layout: "vertical",
34
+ contents: [{
35
+ type: "box",
36
+ layout: "horizontal",
37
+ contents: [{
38
+ type: "box",
39
+ layout: "vertical",
40
+ contents: [],
41
+ width: "4px",
42
+ backgroundColor: "#06C755",
43
+ cornerRadius: "2px"
44
+ }, {
45
+ type: "text",
46
+ text: title,
47
+ weight: "bold",
48
+ size: "xl",
49
+ color: "#111111",
50
+ wrap: true,
51
+ flex: 1,
52
+ margin: "lg"
53
+ }]
54
+ }, {
55
+ type: "box",
56
+ layout: "vertical",
57
+ contents: [{
58
+ type: "text",
59
+ text: body,
60
+ size: "md",
61
+ color: "#444444",
62
+ wrap: true,
63
+ lineSpacing: "6px"
64
+ }],
65
+ margin: "xl",
66
+ paddingAll: "lg",
67
+ backgroundColor: "#F8F9FA",
68
+ cornerRadius: "lg"
69
+ }],
70
+ paddingAll: "xl",
71
+ backgroundColor: "#FFFFFF"
72
+ }
73
+ };
74
+ if (footer) attachFooterText(bubble, footer);
75
+ return bubble;
76
+ }
77
+ /**
78
+ * Create a list card with title and multiple items
79
+ *
80
+ * Editorial design: Numbered/bulleted list with clear visual hierarchy,
81
+ * accent dots for each item, and generous spacing.
82
+ */
83
+ function createListCard(title, items) {
84
+ const itemContents = items.slice(0, 8).map((item, index) => {
85
+ const itemContentsLocal = [{
86
+ type: "text",
87
+ text: item.title,
88
+ size: "md",
89
+ weight: "bold",
90
+ color: "#1a1a1a",
91
+ wrap: true
92
+ }];
93
+ if (item.subtitle) itemContentsLocal.push({
94
+ type: "text",
95
+ text: item.subtitle,
96
+ size: "sm",
97
+ color: "#888888",
98
+ wrap: true,
99
+ margin: "xs"
100
+ });
101
+ const itemBox = {
102
+ type: "box",
103
+ layout: "horizontal",
104
+ contents: [{
105
+ type: "box",
106
+ layout: "vertical",
107
+ contents: [{
108
+ type: "box",
109
+ layout: "vertical",
110
+ contents: [],
111
+ width: "8px",
112
+ height: "8px",
113
+ backgroundColor: index === 0 ? "#06C755" : "#DDDDDD",
114
+ cornerRadius: "4px"
115
+ }],
116
+ width: "20px",
117
+ alignItems: "center",
118
+ paddingTop: "sm"
119
+ }, {
120
+ type: "box",
121
+ layout: "vertical",
122
+ contents: itemContentsLocal,
123
+ flex: 1
124
+ }],
125
+ margin: index > 0 ? "lg" : void 0
126
+ };
127
+ if (item.action) itemBox.action = item.action;
128
+ return itemBox;
129
+ });
130
+ return {
131
+ type: "bubble",
132
+ size: "mega",
133
+ body: {
134
+ type: "box",
135
+ layout: "vertical",
136
+ contents: [
137
+ {
138
+ type: "text",
139
+ text: title,
140
+ weight: "bold",
141
+ size: "xl",
142
+ color: "#111111",
143
+ wrap: true
144
+ },
145
+ {
146
+ type: "separator",
147
+ margin: "lg",
148
+ color: "#EEEEEE"
149
+ },
150
+ {
151
+ type: "box",
152
+ layout: "vertical",
153
+ contents: itemContents,
154
+ margin: "lg"
155
+ }
156
+ ],
157
+ paddingAll: "xl",
158
+ backgroundColor: "#FFFFFF"
159
+ }
160
+ };
161
+ }
162
+ /**
163
+ * Create an image card with image, title, and optional body text
164
+ */
165
+ function createImageCard(imageUrl, title, body, options) {
166
+ const bubble = {
167
+ type: "bubble",
168
+ hero: {
169
+ type: "image",
170
+ url: imageUrl,
171
+ size: "full",
172
+ aspectRatio: options?.aspectRatio ?? "20:13",
173
+ aspectMode: options?.aspectMode ?? "cover",
174
+ action: options?.action
175
+ },
176
+ body: {
177
+ type: "box",
178
+ layout: "vertical",
179
+ contents: [{
180
+ type: "text",
181
+ text: title,
182
+ weight: "bold",
183
+ size: "xl",
184
+ wrap: true
185
+ }],
186
+ paddingAll: "lg"
187
+ }
188
+ };
189
+ if (body && bubble.body) bubble.body.contents.push({
190
+ type: "text",
191
+ text: body,
192
+ size: "md",
193
+ wrap: true,
194
+ margin: "md",
195
+ color: "#666666"
196
+ });
197
+ return bubble;
198
+ }
199
+ /**
200
+ * Create an action card with title, body, and action buttons
201
+ */
202
+ function createActionCard(title, body, actions, options) {
203
+ const bubble = {
204
+ type: "bubble",
205
+ body: {
206
+ type: "box",
207
+ layout: "vertical",
208
+ contents: [{
209
+ type: "text",
210
+ text: title,
211
+ weight: "bold",
212
+ size: "xl",
213
+ wrap: true
214
+ }, {
215
+ type: "text",
216
+ text: body,
217
+ size: "md",
218
+ wrap: true,
219
+ margin: "md",
220
+ color: "#666666"
221
+ }],
222
+ paddingAll: "lg"
223
+ },
224
+ footer: {
225
+ type: "box",
226
+ layout: "vertical",
227
+ contents: actions.slice(0, 4).map((action, index) => ({
228
+ type: "button",
229
+ action: action.action,
230
+ style: index === 0 ? "primary" : "secondary",
231
+ margin: index > 0 ? "sm" : void 0
232
+ })),
233
+ paddingAll: "md"
234
+ }
235
+ };
236
+ if (options?.imageUrl) bubble.hero = {
237
+ type: "image",
238
+ url: options.imageUrl,
239
+ size: "full",
240
+ aspectRatio: options.aspectRatio ?? "20:13",
241
+ aspectMode: "cover"
242
+ };
243
+ return bubble;
244
+ }
245
+ /**
246
+ * Create a carousel container from multiple bubbles
247
+ * LINE allows max 12 bubbles in a carousel
248
+ */
249
+ function createCarousel(bubbles) {
250
+ return {
251
+ type: "carousel",
252
+ contents: bubbles.slice(0, 12)
253
+ };
254
+ }
255
+ /**
256
+ * Create a notification bubble (for alerts, status updates)
257
+ *
258
+ * Editorial design: Bold status indicator with accent color,
259
+ * clear typography, optional icon for context.
260
+ */
261
+ function createNotificationBubble(text, options) {
262
+ const typeColors = {
263
+ info: {
264
+ accent: "#3B82F6",
265
+ bg: "#EFF6FF"
266
+ },
267
+ success: {
268
+ accent: "#06C755",
269
+ bg: "#F0FDF4"
270
+ },
271
+ warning: {
272
+ accent: "#F59E0B",
273
+ bg: "#FFFBEB"
274
+ },
275
+ error: {
276
+ accent: "#EF4444",
277
+ bg: "#FEF2F2"
278
+ }
279
+ }[options?.type ?? "info"];
280
+ const contents = [];
281
+ contents.push({
282
+ type: "box",
283
+ layout: "vertical",
284
+ contents: [],
285
+ width: "4px",
286
+ backgroundColor: typeColors.accent,
287
+ cornerRadius: "2px"
288
+ });
289
+ const textContents = [];
290
+ if (options?.title) textContents.push({
291
+ type: "text",
292
+ text: options.title,
293
+ size: "md",
294
+ weight: "bold",
295
+ color: "#111111",
296
+ wrap: true
297
+ });
298
+ textContents.push({
299
+ type: "text",
300
+ text,
301
+ size: options?.title ? "sm" : "md",
302
+ color: options?.title ? "#666666" : "#333333",
303
+ wrap: true,
304
+ margin: options?.title ? "sm" : void 0
305
+ });
306
+ contents.push({
307
+ type: "box",
308
+ layout: "vertical",
309
+ contents: textContents,
310
+ flex: 1,
311
+ paddingStart: "lg"
312
+ });
313
+ return {
314
+ type: "bubble",
315
+ body: {
316
+ type: "box",
317
+ layout: "horizontal",
318
+ contents,
319
+ paddingAll: "xl",
320
+ backgroundColor: typeColors.bg
321
+ }
322
+ };
323
+ }
324
+ //#endregion
325
+ //#region extensions/line/src/flex-templates/schedule-cards.ts
326
+ function buildTitleSubtitleHeader(params) {
327
+ const { title, subtitle } = params;
328
+ const headerContents = [{
329
+ type: "text",
330
+ text: title,
331
+ weight: "bold",
332
+ size: "xl",
333
+ color: "#111111",
334
+ wrap: true
335
+ }];
336
+ if (subtitle) headerContents.push({
337
+ type: "text",
338
+ text: subtitle,
339
+ size: "sm",
340
+ color: "#888888",
341
+ margin: "sm",
342
+ wrap: true
343
+ });
344
+ return headerContents;
345
+ }
346
+ function buildCardHeaderSections(headerContents) {
347
+ return [{
348
+ type: "box",
349
+ layout: "vertical",
350
+ contents: headerContents,
351
+ paddingBottom: "lg"
352
+ }, {
353
+ type: "separator",
354
+ color: "#EEEEEE"
355
+ }];
356
+ }
357
+ function createMegaBubbleWithFooter(params) {
358
+ const bubble = {
359
+ type: "bubble",
360
+ size: "mega",
361
+ body: {
362
+ type: "box",
363
+ layout: "vertical",
364
+ contents: params.bodyContents,
365
+ paddingAll: "xl",
366
+ backgroundColor: "#FFFFFF"
367
+ }
368
+ };
369
+ if (params.footer) attachFooterText(bubble, params.footer);
370
+ return bubble;
371
+ }
372
+ /**
373
+ * Create a receipt/summary card (for orders, transactions, data tables)
374
+ *
375
+ * Editorial design: Clean table layout with alternating row backgrounds,
376
+ * prominent total section, and clear visual hierarchy.
377
+ */
378
+ function createReceiptCard(params) {
379
+ const { title, subtitle, items, total, footer } = params;
380
+ const itemRows = items.slice(0, 12).map((item, index) => ({
381
+ type: "box",
382
+ layout: "horizontal",
383
+ contents: [{
384
+ type: "text",
385
+ text: item.name,
386
+ size: "sm",
387
+ color: item.highlight ? "#111111" : "#666666",
388
+ weight: item.highlight ? "bold" : "regular",
389
+ flex: 3,
390
+ wrap: true
391
+ }, {
392
+ type: "text",
393
+ text: item.value,
394
+ size: "sm",
395
+ color: item.highlight ? "#06C755" : "#333333",
396
+ weight: item.highlight ? "bold" : "regular",
397
+ flex: 2,
398
+ align: "end",
399
+ wrap: true
400
+ }],
401
+ paddingAll: "md",
402
+ backgroundColor: index % 2 === 0 ? "#FFFFFF" : "#FAFAFA"
403
+ }));
404
+ const bodyContents = [...buildCardHeaderSections(buildTitleSubtitleHeader({
405
+ title,
406
+ subtitle
407
+ })), {
408
+ type: "box",
409
+ layout: "vertical",
410
+ contents: itemRows,
411
+ margin: "md",
412
+ cornerRadius: "md",
413
+ borderWidth: "light",
414
+ borderColor: "#EEEEEE"
415
+ }];
416
+ if (total) bodyContents.push({
417
+ type: "box",
418
+ layout: "horizontal",
419
+ contents: [{
420
+ type: "text",
421
+ text: total.label,
422
+ size: "lg",
423
+ weight: "bold",
424
+ color: "#111111",
425
+ flex: 2
426
+ }, {
427
+ type: "text",
428
+ text: total.value,
429
+ size: "xl",
430
+ weight: "bold",
431
+ color: "#06C755",
432
+ flex: 2,
433
+ align: "end"
434
+ }],
435
+ margin: "xl",
436
+ paddingAll: "lg",
437
+ backgroundColor: "#F0FDF4",
438
+ cornerRadius: "lg"
439
+ });
440
+ return createMegaBubbleWithFooter({
441
+ bodyContents,
442
+ footer
443
+ });
444
+ }
445
+ /**
446
+ * Create a calendar event card (for meetings, appointments, reminders)
447
+ *
448
+ * Editorial design: Date as hero, strong typographic hierarchy,
449
+ * color-blocked zones, full text wrapping for readability.
450
+ */
451
+ function createEventCard(params) {
452
+ const { title, date, time, location, description, calendar, isAllDay, action } = params;
453
+ const dateBlock = {
454
+ type: "box",
455
+ layout: "vertical",
456
+ contents: [{
457
+ type: "text",
458
+ text: date.toUpperCase(),
459
+ size: "sm",
460
+ weight: "bold",
461
+ color: "#06C755",
462
+ wrap: true
463
+ }, {
464
+ type: "text",
465
+ text: isAllDay ? "ALL DAY" : time ?? "",
466
+ size: "xxl",
467
+ weight: "bold",
468
+ color: "#111111",
469
+ wrap: true,
470
+ margin: "xs"
471
+ }],
472
+ paddingBottom: "lg",
473
+ borderWidth: "none"
474
+ };
475
+ if (!time && !isAllDay) dateBlock.contents = [{
476
+ type: "text",
477
+ text: date,
478
+ size: "xl",
479
+ weight: "bold",
480
+ color: "#111111",
481
+ wrap: true
482
+ }];
483
+ const bodyContents = [dateBlock, {
484
+ type: "box",
485
+ layout: "horizontal",
486
+ contents: [{
487
+ type: "box",
488
+ layout: "vertical",
489
+ contents: [],
490
+ width: "4px",
491
+ backgroundColor: "#06C755",
492
+ cornerRadius: "2px"
493
+ }, {
494
+ type: "box",
495
+ layout: "vertical",
496
+ contents: [{
497
+ type: "text",
498
+ text: title,
499
+ size: "lg",
500
+ weight: "bold",
501
+ color: "#1a1a1a",
502
+ wrap: true
503
+ }, ...calendar ? [{
504
+ type: "text",
505
+ text: calendar,
506
+ size: "xs",
507
+ color: "#888888",
508
+ margin: "sm",
509
+ wrap: true
510
+ }] : []],
511
+ flex: 1,
512
+ paddingStart: "lg"
513
+ }],
514
+ paddingTop: "lg",
515
+ paddingBottom: "lg",
516
+ borderWidth: "light",
517
+ borderColor: "#EEEEEE"
518
+ }];
519
+ if (location || description) {
520
+ const detailItems = [];
521
+ if (location) detailItems.push({
522
+ type: "box",
523
+ layout: "horizontal",
524
+ contents: [{
525
+ type: "text",
526
+ text: "📍",
527
+ size: "sm",
528
+ flex: 0
529
+ }, {
530
+ type: "text",
531
+ text: location,
532
+ size: "sm",
533
+ color: "#444444",
534
+ margin: "md",
535
+ flex: 1,
536
+ wrap: true
537
+ }],
538
+ alignItems: "flex-start"
539
+ });
540
+ if (description) detailItems.push({
541
+ type: "text",
542
+ text: description,
543
+ size: "sm",
544
+ color: "#666666",
545
+ wrap: true,
546
+ margin: location ? "lg" : "none"
547
+ });
548
+ bodyContents.push({
549
+ type: "box",
550
+ layout: "vertical",
551
+ contents: detailItems,
552
+ margin: "lg",
553
+ paddingAll: "lg",
554
+ backgroundColor: "#F8F9FA",
555
+ cornerRadius: "lg"
556
+ });
557
+ }
558
+ return {
559
+ type: "bubble",
560
+ size: "mega",
561
+ body: {
562
+ type: "box",
563
+ layout: "vertical",
564
+ contents: bodyContents,
565
+ paddingAll: "xl",
566
+ backgroundColor: "#FFFFFF",
567
+ action
568
+ }
569
+ };
570
+ }
571
+ /**
572
+ * Create a calendar agenda card showing multiple events
573
+ *
574
+ * Editorial timeline design: Time-focused left column with event details
575
+ * on the right. Visual accent bars indicate event priority/recency.
576
+ */
577
+ function createAgendaCard(params) {
578
+ const { title, subtitle, events, footer } = params;
579
+ const headerContents = buildTitleSubtitleHeader({
580
+ title,
581
+ subtitle
582
+ });
583
+ const eventItems = events.slice(0, 6).map((event, index) => {
584
+ const isActive = event.isNow || index === 0;
585
+ const accentColor = isActive ? "#06C755" : "#E5E5E5";
586
+ const timeColumn = {
587
+ type: "box",
588
+ layout: "vertical",
589
+ contents: [{
590
+ type: "text",
591
+ text: event.time ?? "—",
592
+ size: "sm",
593
+ weight: isActive ? "bold" : "regular",
594
+ color: isActive ? "#06C755" : "#666666",
595
+ align: "end",
596
+ wrap: true
597
+ }],
598
+ width: "65px",
599
+ justifyContent: "flex-start"
600
+ };
601
+ const dotColumn = {
602
+ type: "box",
603
+ layout: "vertical",
604
+ contents: [{
605
+ type: "box",
606
+ layout: "vertical",
607
+ contents: [],
608
+ width: "10px",
609
+ height: "10px",
610
+ backgroundColor: accentColor,
611
+ cornerRadius: "5px"
612
+ }],
613
+ width: "24px",
614
+ alignItems: "center",
615
+ justifyContent: "flex-start",
616
+ paddingTop: "xs"
617
+ };
618
+ const detailContents = [{
619
+ type: "text",
620
+ text: event.title,
621
+ size: "md",
622
+ weight: "bold",
623
+ color: "#1a1a1a",
624
+ wrap: true
625
+ }];
626
+ const secondaryParts = [];
627
+ if (event.location) secondaryParts.push(event.location);
628
+ if (event.calendar) secondaryParts.push(event.calendar);
629
+ if (secondaryParts.length > 0) detailContents.push({
630
+ type: "text",
631
+ text: secondaryParts.join(" · "),
632
+ size: "xs",
633
+ color: "#888888",
634
+ wrap: true,
635
+ margin: "xs"
636
+ });
637
+ return {
638
+ type: "box",
639
+ layout: "horizontal",
640
+ contents: [
641
+ timeColumn,
642
+ dotColumn,
643
+ {
644
+ type: "box",
645
+ layout: "vertical",
646
+ contents: detailContents,
647
+ flex: 1
648
+ }
649
+ ],
650
+ margin: index > 0 ? "xl" : void 0,
651
+ alignItems: "flex-start"
652
+ };
653
+ });
654
+ return createMegaBubbleWithFooter({
655
+ bodyContents: [...buildCardHeaderSections(headerContents), {
656
+ type: "box",
657
+ layout: "vertical",
658
+ contents: eventItems,
659
+ paddingTop: "xl"
660
+ }],
661
+ footer
662
+ });
663
+ }
664
+ function truncateLineActionLabel(label, limit = 20) {
665
+ return truncateUtf16Safe(label, limit);
666
+ }
667
+ function truncateLineActionData(data) {
668
+ return truncateUtf16Safe(data, 300);
669
+ }
670
+ /**
671
+ * Create a message action (sends text when tapped)
672
+ */
673
+ function messageAction(label, text) {
674
+ return {
675
+ type: "message",
676
+ label: truncateLineActionLabel(label),
677
+ text: text ?? label
678
+ };
679
+ }
680
+ /**
681
+ * Create a URI action (opens a URL when tapped)
682
+ */
683
+ function uriAction(label, uri) {
684
+ return {
685
+ type: "uri",
686
+ label: truncateLineActionLabel(label),
687
+ uri
688
+ };
689
+ }
690
+ /**
691
+ * Create a postback action (sends data to webhook when tapped)
692
+ */
693
+ function postbackAction(label, data, displayText) {
694
+ return {
695
+ type: "postback",
696
+ label: truncateLineActionLabel(label),
697
+ data: truncateLineActionData(data),
698
+ displayText: displayText === void 0 ? void 0 : truncateLineActionData(displayText)
699
+ };
700
+ }
701
+ /**
702
+ * Create a datetime picker action
703
+ */
704
+ function datetimePickerAction(label, data, mode, options) {
705
+ return {
706
+ type: "datetimepicker",
707
+ label: truncateLineActionLabel(label),
708
+ data: truncateLineActionData(data),
709
+ mode,
710
+ initial: options?.initial,
711
+ max: options?.max,
712
+ min: options?.min
713
+ };
714
+ }
715
+ //#endregion
716
+ //#region extensions/line/src/flex-templates/media-control-cards.ts
717
+ /**
718
+ * Create a media player card for Sonos, Spotify, Apple Music, etc.
719
+ *
720
+ * Editorial design: Album art hero with gradient overlay for text,
721
+ * prominent now-playing indicator, refined playback controls.
722
+ */
723
+ function createMediaPlayerCard(params) {
724
+ const { title, subtitle, source, imageUrl, isPlaying, progress, controls, extraActions } = params;
725
+ const trackInfo = [{
726
+ type: "text",
727
+ text: title,
728
+ weight: "bold",
729
+ size: "xl",
730
+ color: "#111111",
731
+ wrap: true
732
+ }];
733
+ if (subtitle) trackInfo.push({
734
+ type: "text",
735
+ text: subtitle,
736
+ size: "md",
737
+ color: "#666666",
738
+ wrap: true,
739
+ margin: "sm"
740
+ });
741
+ const statusItems = [];
742
+ if (isPlaying !== void 0) statusItems.push({
743
+ type: "box",
744
+ layout: "horizontal",
745
+ contents: [{
746
+ type: "box",
747
+ layout: "vertical",
748
+ contents: [],
749
+ width: "8px",
750
+ height: "8px",
751
+ backgroundColor: isPlaying ? "#06C755" : "#CCCCCC",
752
+ cornerRadius: "4px"
753
+ }, {
754
+ type: "text",
755
+ text: isPlaying ? "Now Playing" : "Paused",
756
+ size: "xs",
757
+ color: isPlaying ? "#06C755" : "#888888",
758
+ weight: "bold",
759
+ margin: "sm"
760
+ }],
761
+ alignItems: "center"
762
+ });
763
+ if (source) statusItems.push({
764
+ type: "text",
765
+ text: source,
766
+ size: "xs",
767
+ color: "#AAAAAA",
768
+ margin: statusItems.length > 0 ? "lg" : void 0
769
+ });
770
+ if (progress) statusItems.push({
771
+ type: "text",
772
+ text: progress,
773
+ size: "xs",
774
+ color: "#888888",
775
+ align: "end",
776
+ flex: 1
777
+ });
778
+ const bodyContents = [{
779
+ type: "box",
780
+ layout: "vertical",
781
+ contents: trackInfo
782
+ }];
783
+ if (statusItems.length > 0) bodyContents.push({
784
+ type: "box",
785
+ layout: "horizontal",
786
+ contents: statusItems,
787
+ margin: "lg",
788
+ alignItems: "center"
789
+ });
790
+ const bubble = {
791
+ type: "bubble",
792
+ size: "mega",
793
+ body: {
794
+ type: "box",
795
+ layout: "vertical",
796
+ contents: bodyContents,
797
+ paddingAll: "xl",
798
+ backgroundColor: "#FFFFFF"
799
+ }
800
+ };
801
+ if (imageUrl) bubble.hero = {
802
+ type: "image",
803
+ url: imageUrl,
804
+ size: "full",
805
+ aspectRatio: "1:1",
806
+ aspectMode: "cover"
807
+ };
808
+ if (controls || extraActions?.length) {
809
+ const footerContents = [];
810
+ if (controls) {
811
+ const controlButtons = [];
812
+ if (controls.previous) controlButtons.push({
813
+ type: "button",
814
+ action: {
815
+ type: "postback",
816
+ label: "⏮",
817
+ data: controls.previous.data
818
+ },
819
+ style: "secondary",
820
+ flex: 1,
821
+ height: "sm"
822
+ });
823
+ if (controls.play) controlButtons.push({
824
+ type: "button",
825
+ action: {
826
+ type: "postback",
827
+ label: "▶",
828
+ data: controls.play.data
829
+ },
830
+ style: isPlaying ? "secondary" : "primary",
831
+ flex: 1,
832
+ height: "sm",
833
+ margin: controls.previous ? "md" : void 0
834
+ });
835
+ if (controls.pause) controlButtons.push({
836
+ type: "button",
837
+ action: {
838
+ type: "postback",
839
+ label: "⏸",
840
+ data: controls.pause.data
841
+ },
842
+ style: isPlaying ? "primary" : "secondary",
843
+ flex: 1,
844
+ height: "sm",
845
+ margin: controlButtons.length > 0 ? "md" : void 0
846
+ });
847
+ if (controls.next) controlButtons.push({
848
+ type: "button",
849
+ action: {
850
+ type: "postback",
851
+ label: "⏭",
852
+ data: controls.next.data
853
+ },
854
+ style: "secondary",
855
+ flex: 1,
856
+ height: "sm",
857
+ margin: controlButtons.length > 0 ? "md" : void 0
858
+ });
859
+ if (controlButtons.length > 0) footerContents.push({
860
+ type: "box",
861
+ layout: "horizontal",
862
+ contents: controlButtons
863
+ });
864
+ }
865
+ if (extraActions?.length) footerContents.push({
866
+ type: "box",
867
+ layout: "horizontal",
868
+ contents: extraActions.slice(0, 2).map((action, index) => ({
869
+ type: "button",
870
+ action: {
871
+ type: "postback",
872
+ label: truncateLineActionLabel(action.label, 15),
873
+ data: action.data
874
+ },
875
+ style: "secondary",
876
+ flex: 1,
877
+ height: "sm",
878
+ margin: index > 0 ? "md" : void 0
879
+ })),
880
+ margin: "md"
881
+ });
882
+ if (footerContents.length > 0) bubble.footer = {
883
+ type: "box",
884
+ layout: "vertical",
885
+ contents: footerContents,
886
+ paddingAll: "lg",
887
+ backgroundColor: "#FAFAFA"
888
+ };
889
+ }
890
+ return bubble;
891
+ }
892
+ /**
893
+ * Create an Apple TV remote card with a D-pad and control rows.
894
+ */
895
+ function createAppleTvRemoteCard(params) {
896
+ const { deviceName, status, actionData } = params;
897
+ const headerContents = [{
898
+ type: "text",
899
+ text: deviceName,
900
+ weight: "bold",
901
+ size: "xl",
902
+ color: "#111111",
903
+ wrap: true
904
+ }];
905
+ if (status) headerContents.push({
906
+ type: "text",
907
+ text: status,
908
+ size: "sm",
909
+ color: "#666666",
910
+ wrap: true,
911
+ margin: "sm"
912
+ });
913
+ const makeButton = (label, data, style = "secondary") => ({
914
+ type: "button",
915
+ action: {
916
+ type: "postback",
917
+ label,
918
+ data
919
+ },
920
+ style,
921
+ height: "sm",
922
+ flex: 1
923
+ });
924
+ const dpadRows = [
925
+ {
926
+ type: "box",
927
+ layout: "horizontal",
928
+ contents: [
929
+ { type: "filler" },
930
+ makeButton("↑", actionData.up),
931
+ { type: "filler" }
932
+ ]
933
+ },
934
+ {
935
+ type: "box",
936
+ layout: "horizontal",
937
+ contents: [
938
+ makeButton("←", actionData.left),
939
+ makeButton("OK", actionData.select, "primary"),
940
+ makeButton("→", actionData.right)
941
+ ],
942
+ margin: "md"
943
+ },
944
+ {
945
+ type: "box",
946
+ layout: "horizontal",
947
+ contents: [
948
+ { type: "filler" },
949
+ makeButton("↓", actionData.down),
950
+ { type: "filler" }
951
+ ],
952
+ margin: "md"
953
+ }
954
+ ];
955
+ const menuRow = {
956
+ type: "box",
957
+ layout: "horizontal",
958
+ contents: [makeButton("Menu", actionData.menu), makeButton("Home", actionData.home)],
959
+ margin: "lg"
960
+ };
961
+ const playbackRow = {
962
+ type: "box",
963
+ layout: "horizontal",
964
+ contents: [makeButton("Play", actionData.play), makeButton("Pause", actionData.pause)],
965
+ margin: "md"
966
+ };
967
+ const volumeRow = {
968
+ type: "box",
969
+ layout: "horizontal",
970
+ contents: [
971
+ makeButton("Vol +", actionData.volumeUp),
972
+ makeButton("Mute", actionData.mute),
973
+ makeButton("Vol -", actionData.volumeDown)
974
+ ],
975
+ margin: "md"
976
+ };
977
+ return {
978
+ type: "bubble",
979
+ size: "mega",
980
+ body: {
981
+ type: "box",
982
+ layout: "vertical",
983
+ contents: [
984
+ {
985
+ type: "box",
986
+ layout: "vertical",
987
+ contents: headerContents
988
+ },
989
+ {
990
+ type: "separator",
991
+ margin: "lg",
992
+ color: "#EEEEEE"
993
+ },
994
+ ...dpadRows,
995
+ menuRow,
996
+ playbackRow,
997
+ volumeRow
998
+ ],
999
+ paddingAll: "xl",
1000
+ backgroundColor: "#FFFFFF"
1001
+ }
1002
+ };
1003
+ }
1004
+ /**
1005
+ * Create a device control card for Apple TV, smart home devices, etc.
1006
+ *
1007
+ * Editorial design: Device-focused header with status indicator,
1008
+ * clean control grid with clear visual hierarchy.
1009
+ */
1010
+ function createDeviceControlCard(params) {
1011
+ const { deviceName, deviceType, status, isOnline, imageUrl, controls } = params;
1012
+ const headerContents = [{
1013
+ type: "box",
1014
+ layout: "horizontal",
1015
+ contents: [{
1016
+ type: "box",
1017
+ layout: "vertical",
1018
+ contents: [],
1019
+ width: "10px",
1020
+ height: "10px",
1021
+ backgroundColor: isOnline !== false ? "#06C755" : "#FF5555",
1022
+ cornerRadius: "5px"
1023
+ }, {
1024
+ type: "text",
1025
+ text: deviceName,
1026
+ weight: "bold",
1027
+ size: "xl",
1028
+ color: "#111111",
1029
+ wrap: true,
1030
+ flex: 1,
1031
+ margin: "md"
1032
+ }],
1033
+ alignItems: "center"
1034
+ }];
1035
+ if (deviceType) headerContents.push({
1036
+ type: "text",
1037
+ text: deviceType,
1038
+ size: "sm",
1039
+ color: "#888888",
1040
+ margin: "sm"
1041
+ });
1042
+ if (status) headerContents.push({
1043
+ type: "box",
1044
+ layout: "vertical",
1045
+ contents: [{
1046
+ type: "text",
1047
+ text: status,
1048
+ size: "sm",
1049
+ color: "#444444",
1050
+ wrap: true
1051
+ }],
1052
+ margin: "lg",
1053
+ paddingAll: "md",
1054
+ backgroundColor: "#F8F9FA",
1055
+ cornerRadius: "md"
1056
+ });
1057
+ const bubble = {
1058
+ type: "bubble",
1059
+ size: "mega",
1060
+ body: {
1061
+ type: "box",
1062
+ layout: "vertical",
1063
+ contents: headerContents,
1064
+ paddingAll: "xl",
1065
+ backgroundColor: "#FFFFFF"
1066
+ }
1067
+ };
1068
+ if (imageUrl) bubble.hero = {
1069
+ type: "image",
1070
+ url: imageUrl,
1071
+ size: "full",
1072
+ aspectRatio: "16:9",
1073
+ aspectMode: "cover"
1074
+ };
1075
+ if (controls.length > 0) {
1076
+ const rows = [];
1077
+ const limitedControls = controls.slice(0, 6);
1078
+ for (let i = 0; i < limitedControls.length; i += 2) {
1079
+ const rowButtons = [];
1080
+ for (let j = i; j < Math.min(i + 2, limitedControls.length); j++) {
1081
+ const ctrl = limitedControls[j];
1082
+ const buttonLabel = ctrl.icon ? `${ctrl.icon} ${ctrl.label}` : ctrl.label;
1083
+ rowButtons.push({
1084
+ type: "button",
1085
+ action: {
1086
+ type: "postback",
1087
+ label: truncateLineActionLabel(buttonLabel, 18),
1088
+ data: ctrl.data
1089
+ },
1090
+ style: ctrl.style ?? "secondary",
1091
+ flex: 1,
1092
+ height: "sm",
1093
+ margin: j > i ? "md" : void 0
1094
+ });
1095
+ }
1096
+ if (rowButtons.length === 1) rowButtons.push({ type: "filler" });
1097
+ rows.push({
1098
+ type: "box",
1099
+ layout: "horizontal",
1100
+ contents: rowButtons,
1101
+ margin: i > 0 ? "md" : void 0
1102
+ });
1103
+ }
1104
+ bubble.footer = {
1105
+ type: "box",
1106
+ layout: "vertical",
1107
+ contents: rows,
1108
+ paddingAll: "lg",
1109
+ backgroundColor: "#FAFAFA"
1110
+ };
1111
+ }
1112
+ return bubble;
1113
+ }
1114
+ //#endregion
1115
+ //#region extensions/line/src/flex-templates/message.ts
1116
+ /**
1117
+ * Wrap a FlexContainer in a FlexMessage
1118
+ */
1119
+ function toFlexMessage(altText, contents) {
1120
+ return {
1121
+ type: "flex",
1122
+ altText,
1123
+ contents
1124
+ };
1125
+ }
1126
+ //#endregion
1127
+ export { createNotificationBubble as _, datetimePickerAction as a, uriAction as c, createReceiptCard as d, createActionCard as f, createListCard as g, createInfoCard as h, createMediaPlayerCard as i, createAgendaCard as l, createImageCard as m, createAppleTvRemoteCard as n, messageAction as o, createCarousel as p, createDeviceControlCard as r, postbackAction as s, toFlexMessage as t, createEventCard as u };