odt-ui-rails 2.2.0 → 2.2.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.
@@ -1173,3 +1173,423 @@
1173
1173
  color: var(--color-fg-muted);
1174
1174
  line-height: 1rem;
1175
1175
  }
1176
+
1177
+ /* ==========================================================================
1178
+ * 10. Toast & Toaster Component (.odt-toaster, .odt-toast)
1179
+ * ========================================================================== */
1180
+ .odt-toaster {
1181
+ position: fixed;
1182
+ z-index: 9999;
1183
+ box-sizing: border-box;
1184
+ font-family: inherit;
1185
+ width: 100%;
1186
+ max-width: 24rem; /* 384px */
1187
+ padding: var(--spacing-4, 1rem);
1188
+ pointer-events: none;
1189
+ display: flex;
1190
+ flex-direction: column;
1191
+ gap: var(--spacing-2-5, 0.625rem);
1192
+ }
1193
+
1194
+ /* Positions */
1195
+ .odt-toaster--top-right {
1196
+ top: 0;
1197
+ right: 0;
1198
+ }
1199
+
1200
+ .odt-toaster--top-center {
1201
+ top: 0;
1202
+ left: 50%;
1203
+ transform: translateX(-50%);
1204
+ }
1205
+
1206
+ .odt-toaster--top-left {
1207
+ top: 0;
1208
+ left: 0;
1209
+ }
1210
+
1211
+ .odt-toaster--bottom-right {
1212
+ bottom: 0;
1213
+ right: 0;
1214
+ flex-direction: column-reverse;
1215
+ }
1216
+
1217
+ .odt-toaster--bottom-center {
1218
+ bottom: 0;
1219
+ left: 50%;
1220
+ transform: translateX(-50%);
1221
+ flex-direction: column-reverse;
1222
+ }
1223
+
1224
+ .odt-toaster--bottom-left {
1225
+ bottom: 0;
1226
+ left: 0;
1227
+ flex-direction: column-reverse;
1228
+ }
1229
+
1230
+ /* Toast Item */
1231
+ .odt-toast {
1232
+ position: relative;
1233
+ width: 100%;
1234
+ box-sizing: border-box;
1235
+ display: flex;
1236
+ align-items: flex-start;
1237
+ gap: var(--spacing-3, 0.75rem);
1238
+ padding: var(--spacing-3-5, 0.875rem) var(--spacing-4, 1rem);
1239
+ background-color: var(--color-surface-elevated, #ffffff);
1240
+ color: var(--color-fg);
1241
+ border-radius: var(--radius-2xl, 1rem);
1242
+ box-shadow: 0 8px 24px -4px rgba(0, 0, 0, 0.08), 0 2px 6px -2px rgba(0, 0, 0, 0.04);
1243
+ border: 1px solid var(--color-line-muted, #f4f4f5);
1244
+ pointer-events: auto;
1245
+ user-select: none;
1246
+ font-family: inherit;
1247
+ overflow: hidden;
1248
+ animation: odt-toast-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
1249
+ transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease, margin 0.25s ease;
1250
+ }
1251
+
1252
+ .odt-toast--exiting {
1253
+ animation: odt-toast-exit 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
1254
+ }
1255
+
1256
+ @keyframes odt-toast-enter {
1257
+ 0% {
1258
+ opacity: 0;
1259
+ transform: translateY(-12px) scale(0.96);
1260
+ }
1261
+ 100% {
1262
+ opacity: 1;
1263
+ transform: translateY(0) scale(1);
1264
+ }
1265
+ }
1266
+
1267
+ @keyframes odt-toast-exit {
1268
+ 0% {
1269
+ opacity: 1;
1270
+ transform: translateY(0) scale(1);
1271
+ }
1272
+ 100% {
1273
+ opacity: 0;
1274
+ transform: translateY(-8px) scale(0.94);
1275
+ }
1276
+ }
1277
+
1278
+ .odt-toaster--bottom-right .odt-toast,
1279
+ .odt-toaster--bottom-center .odt-toast,
1280
+ .odt-toaster--bottom-left .odt-toast {
1281
+ animation-name: odt-toast-enter-bottom;
1282
+ }
1283
+
1284
+ .odt-toaster--bottom-right .odt-toast--exiting,
1285
+ .odt-toaster--bottom-center .odt-toast--exiting,
1286
+ .odt-toaster--bottom-left .odt-toast--exiting {
1287
+ animation-name: odt-toast-exit-bottom;
1288
+ }
1289
+
1290
+ @keyframes odt-toast-enter-bottom {
1291
+ 0% {
1292
+ opacity: 0;
1293
+ transform: translateY(12px) scale(0.96);
1294
+ }
1295
+ 100% {
1296
+ opacity: 1;
1297
+ transform: translateY(0) scale(1);
1298
+ }
1299
+ }
1300
+
1301
+ @keyframes odt-toast-exit-bottom {
1302
+ 0% {
1303
+ opacity: 1;
1304
+ transform: translateY(0) scale(1);
1305
+ }
1306
+ 100% {
1307
+ opacity: 0;
1308
+ transform: translateY(8px) scale(0.94);
1309
+ }
1310
+ }
1311
+
1312
+ /* Toast Variants / Semantic Themes */
1313
+ .odt-toast--success {
1314
+ background-color: var(--color-surface-success, hsl(142 71% 96%));
1315
+ border-color: var(--color-line-success, hsl(142 71% 85%));
1316
+ }
1317
+
1318
+ .odt-toast--warning {
1319
+ background-color: var(--color-surface-warning, hsl(38 92% 96%));
1320
+ border-color: var(--color-line-warning, hsl(38 92% 85%));
1321
+ }
1322
+
1323
+ .odt-toast--danger,
1324
+ .odt-toast--error {
1325
+ background-color: var(--color-surface-danger, hsl(0 84% 97%));
1326
+ border-color: var(--color-line-danger, hsl(0 84% 88%));
1327
+ }
1328
+
1329
+ .odt-toast--info {
1330
+ background-color: var(--color-surface-info, hsl(217 91% 97%));
1331
+ border-color: var(--color-info, hsl(217 91% 88%));
1332
+ }
1333
+
1334
+ .odt-toast--frosted {
1335
+ background-color: var(--frosted-surface-bg, rgba(255, 255, 255, 0.70));
1336
+ backdrop-filter: blur(16px);
1337
+ -webkit-backdrop-filter: blur(16px);
1338
+ border: 1px solid var(--frosted-surface-border, rgba(255, 255, 255, 0.80));
1339
+ }
1340
+
1341
+ /* Toast Sub-elements */
1342
+ .odt-toast__icon {
1343
+ display: flex;
1344
+ align-items: center;
1345
+ justify-content: center;
1346
+ width: 1.375rem;
1347
+ height: 1.375rem;
1348
+ flex-shrink: 0;
1349
+ font-size: 1.125rem;
1350
+ margin-top: 0.0625rem;
1351
+ }
1352
+
1353
+ .odt-toast__icon--success { color: var(--color-success, #16a34a); }
1354
+ .odt-toast__icon--warning { color: var(--color-warning, #d97706); }
1355
+ .odt-toast__icon--danger,
1356
+ .odt-toast__icon--error { color: var(--color-danger, #dc2626); }
1357
+ .odt-toast__icon--info { color: var(--color-info, #2563eb); }
1358
+ .odt-toast__icon--loading {
1359
+ color: var(--color-primary-500, #3b82f6);
1360
+ animation: odt-spin 0.8s linear infinite;
1361
+ }
1362
+
1363
+ .odt-toast__content {
1364
+ display: flex;
1365
+ flex-direction: column;
1366
+ gap: var(--spacing-0-5, 0.125rem);
1367
+ flex: 1;
1368
+ min-width: 0;
1369
+ }
1370
+
1371
+ .odt-toast__title {
1372
+ font-size: var(--font-size-sm, 0.875rem);
1373
+ font-weight: var(--font-weight-semibold, 600);
1374
+ color: var(--color-fg-strong);
1375
+ line-height: 1.35;
1376
+ margin: 0;
1377
+ }
1378
+
1379
+ .odt-toast__message,
1380
+ .odt-toast__description {
1381
+ font-size: var(--font-size-xs, 0.75rem);
1382
+ color: var(--color-fg-muted);
1383
+ line-height: 1.4;
1384
+ margin: 0;
1385
+ }
1386
+
1387
+ .odt-toast__close {
1388
+ display: inline-flex;
1389
+ align-items: center;
1390
+ justify-content: center;
1391
+ width: 1.5rem;
1392
+ height: 1.5rem;
1393
+ padding: 0;
1394
+ border-radius: var(--radius-md, 0.375rem);
1395
+ border: none;
1396
+ background: transparent;
1397
+ color: var(--color-fg-muted);
1398
+ cursor: pointer;
1399
+ flex-shrink: 0;
1400
+ margin-left: auto;
1401
+ transition: background-color 0.15s ease, color 0.15s ease;
1402
+ font-size: 1.125rem;
1403
+ line-height: 1;
1404
+ }
1405
+
1406
+ .odt-toast__close:hover {
1407
+ background-color: var(--color-surface-subtle);
1408
+ color: var(--color-fg-strong);
1409
+ }
1410
+
1411
+ /* ==========================================================================
1412
+ * 11. Modal Component (.odt-modal-overlay, .odt-modal)
1413
+ * ========================================================================== */
1414
+ @keyframes odt-overlay-fade-in {
1415
+ from { opacity: 0; }
1416
+ to { opacity: 1; }
1417
+ }
1418
+
1419
+ @keyframes odt-overlay-fade-out {
1420
+ from { opacity: 1; }
1421
+ to { opacity: 0; }
1422
+ }
1423
+
1424
+ @keyframes odt-modal-scale-in {
1425
+ from {
1426
+ opacity: 0;
1427
+ transform: scale(0.95) translateY(10px);
1428
+ }
1429
+ to {
1430
+ opacity: 1;
1431
+ transform: scale(1) translateY(0);
1432
+ }
1433
+ }
1434
+
1435
+ @keyframes odt-modal-scale-out {
1436
+ from {
1437
+ opacity: 1;
1438
+ transform: scale(1) translateY(0);
1439
+ }
1440
+ to {
1441
+ opacity: 0;
1442
+ transform: scale(0.96) translateY(8px);
1443
+ }
1444
+ }
1445
+
1446
+ .odt-modal-overlay {
1447
+ position: fixed;
1448
+ inset: 0;
1449
+ z-index: 1000;
1450
+ display: flex;
1451
+ justify-content: center;
1452
+ align-items: center;
1453
+ padding: var(--spacing-4, 1rem);
1454
+ background-color: var(--color-overlay-backdrop, rgba(0, 0, 0, 0.6));
1455
+ overflow-y: auto;
1456
+ box-sizing: border-box;
1457
+ font-family: inherit;
1458
+ animation: odt-overlay-fade-in 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
1459
+ }
1460
+
1461
+ .odt-modal-overlay.hidden {
1462
+ display: none !important;
1463
+ }
1464
+
1465
+ .odt-modal-overlay--exiting {
1466
+ animation: odt-overlay-fade-out 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
1467
+ pointer-events: none;
1468
+ }
1469
+
1470
+ /* Modal Content Box */
1471
+ .odt-modal {
1472
+ position: relative;
1473
+ width: 100%;
1474
+ box-sizing: border-box;
1475
+ font-family: inherit;
1476
+ background-color: var(--color-surface-elevated, #ffffff);
1477
+ color: var(--color-fg);
1478
+ border-radius: var(--radius-2xl, 1rem);
1479
+ box-shadow: var(--shadow-modal);
1480
+ border: 1px solid var(--color-line-muted, rgba(226, 232, 240, 0.8));
1481
+ display: flex;
1482
+ flex-direction: column;
1483
+ outline: none;
1484
+ animation: odt-modal-scale-in 0.24s cubic-bezier(0.16, 1, 0.3, 1) forwards;
1485
+ margin: auto 0;
1486
+ }
1487
+
1488
+ .odt-modal--exiting {
1489
+ animation: odt-modal-scale-out 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
1490
+ }
1491
+
1492
+ /* Modal Sizes */
1493
+ .odt-modal--sm {
1494
+ max-width: 24rem; /* 384px */
1495
+ }
1496
+
1497
+ .odt-modal--md {
1498
+ max-width: 32rem; /* 512px */
1499
+ }
1500
+
1501
+ .odt-modal--lg {
1502
+ max-width: 42rem; /* 672px */
1503
+ }
1504
+
1505
+ .odt-modal--xl {
1506
+ max-width: 56rem; /* 896px */
1507
+ }
1508
+
1509
+ .odt-modal--full {
1510
+ max-width: calc(100vw - 2rem);
1511
+ min-height: calc(100vh - 3rem);
1512
+ }
1513
+
1514
+ /* Modal Sub-components */
1515
+ .odt-modal__header {
1516
+ display: flex;
1517
+ align-items: flex-start;
1518
+ justify-content: space-between;
1519
+ padding: var(--spacing-6, 1.5rem) var(--spacing-6, 1.5rem) var(--spacing-2, 0.5rem);
1520
+ gap: var(--spacing-3, 0.75rem);
1521
+ }
1522
+
1523
+ .odt-modal__header-content {
1524
+ display: flex;
1525
+ flex-direction: column;
1526
+ gap: var(--spacing-1, 0.25rem);
1527
+ flex: 1;
1528
+ }
1529
+
1530
+ .odt-modal__title {
1531
+ font-family: inherit;
1532
+ font-size: var(--font-size-xl, 1.25rem);
1533
+ font-weight: var(--font-weight-bold, 700);
1534
+ color: var(--color-fg-strong);
1535
+ margin: 0;
1536
+ line-height: var(--line-height-tight, 1.25);
1537
+ }
1538
+
1539
+ .odt-modal__subtitle {
1540
+ font-family: inherit;
1541
+ font-size: var(--font-size-sm, 0.875rem);
1542
+ color: var(--color-fg-muted);
1543
+ margin: 0;
1544
+ }
1545
+
1546
+ .odt-modal__body {
1547
+ font-family: inherit;
1548
+ padding: var(--spacing-2, 0.5rem) var(--spacing-6, 1.5rem) var(--spacing-6, 1.5rem);
1549
+ font-size: var(--font-size-base, 1rem);
1550
+ line-height: var(--line-height-normal, 1.5);
1551
+ color: var(--color-fg);
1552
+ overflow-y: auto;
1553
+ max-height: 70vh;
1554
+ }
1555
+
1556
+ .odt-modal__footer {
1557
+ font-family: inherit;
1558
+ display: flex;
1559
+ align-items: center;
1560
+ justify-content: flex-end;
1561
+ gap: var(--spacing-3, 0.75rem);
1562
+ padding: var(--spacing-2, 0.5rem) var(--spacing-6, 1.5rem) var(--spacing-6, 1.5rem);
1563
+ border-bottom-left-radius: inherit;
1564
+ border-bottom-right-radius: inherit;
1565
+ }
1566
+
1567
+ .odt-modal__close {
1568
+ display: inline-flex;
1569
+ align-items: center;
1570
+ justify-content: center;
1571
+ width: 2rem;
1572
+ height: 2rem;
1573
+ padding: 0;
1574
+ border-radius: var(--radius-lg, 0.5rem);
1575
+ border: none;
1576
+ background: transparent;
1577
+ color: var(--color-fg-muted);
1578
+ cursor: pointer;
1579
+ transition: background-color 0.15s ease, color 0.15s ease, transform 0.1s ease;
1580
+ flex-shrink: 0;
1581
+ margin-left: auto;
1582
+ font-size: 1.25rem;
1583
+ line-height: 1;
1584
+ }
1585
+
1586
+ .odt-modal__close:hover {
1587
+ background-color: var(--color-surface-subtle);
1588
+ color: var(--color-fg-strong);
1589
+ }
1590
+
1591
+ .odt-modal__close:active {
1592
+ transform: scale(0.94);
1593
+ }
1594
+
1595
+
@@ -0,0 +1,61 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["dialog"]
5
+ static values = {
6
+ open: { type: Boolean, default: false },
7
+ preventBackdropClose: { type: Boolean, default: false }
8
+ }
9
+
10
+ connect() {
11
+ if (this.openValue) {
12
+ this.open()
13
+ }
14
+ }
15
+
16
+ open(event) {
17
+ if (event) event.preventDefault()
18
+
19
+ this.element.classList.remove("hidden")
20
+ this.element.setAttribute("aria-hidden", "false")
21
+ this.element.classList.remove("odt-modal-overlay--exiting")
22
+ if (this.hasDialogTarget) {
23
+ this.dialogTarget.classList.remove("odt-modal--exiting")
24
+ }
25
+
26
+ document.body.style.overflow = "hidden"
27
+ this.element.dispatchEvent(new CustomEvent("odt-modal:opened", { bubbles: true }))
28
+ }
29
+
30
+ close(event) {
31
+ if (event) event.preventDefault()
32
+
33
+ this.element.classList.add("odt-modal-overlay--exiting")
34
+ if (this.hasDialogTarget) {
35
+ this.dialogTarget.classList.add("odt-modal--exiting")
36
+ }
37
+
38
+ document.body.style.overflow = ""
39
+
40
+ setTimeout(() => {
41
+ this.element.classList.add("hidden")
42
+ this.element.classList.remove("odt-modal-overlay--exiting")
43
+ if (this.hasDialogTarget) {
44
+ this.dialogTarget.classList.remove("odt-modal--exiting")
45
+ }
46
+ this.element.setAttribute("aria-hidden", "true")
47
+ this.element.dispatchEvent(new CustomEvent("odt-modal:closed", { bubbles: true }))
48
+ }, 200)
49
+ }
50
+
51
+ backdropClose(event) {
52
+ if (this.preventBackdropCloseValue) return
53
+ if (event.target === this.element) {
54
+ this.close(event)
55
+ }
56
+ }
57
+
58
+ disconnect() {
59
+ document.body.style.overflow = ""
60
+ }
61
+ }
@@ -0,0 +1,40 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ duration: { type: Number, default: 4000 }
6
+ }
7
+
8
+ connect() {
9
+ this.startTimer()
10
+ }
11
+
12
+ startTimer() {
13
+ if (this.durationValue > 0) {
14
+ this.timeout = setTimeout(() => this.dismiss(), this.durationValue)
15
+ }
16
+ }
17
+
18
+ pauseTimer() {
19
+ if (this.timeout) {
20
+ clearTimeout(this.timeout)
21
+ this.timeout = null
22
+ }
23
+ }
24
+
25
+ resumeTimer() {
26
+ this.startTimer()
27
+ }
28
+
29
+ dismiss() {
30
+ this.pauseTimer()
31
+ this.element.classList.add("odt-toast--exiting")
32
+ setTimeout(() => {
33
+ this.element.remove()
34
+ }, 250)
35
+ }
36
+
37
+ disconnect() {
38
+ this.pauseTimer()
39
+ }
40
+ }
@@ -65,6 +65,23 @@ module Odt
65
65
  copy_file source, "sig/odt_ui.rbs"
66
66
  say "Copied sig/odt_ui.rbs for Ruby LSP / Solargraph support", :green
67
67
  end
68
+
69
+ def copy_stimulus_controller
70
+ toast_source = File.expand_path("templates/odt_toast_controller.js", __dir__)
71
+ modal_source = File.expand_path("templates/odt_modal_controller.js", __dir__)
72
+
73
+ if File.directory?("app/javascript/controllers") || File.directory?("app/javascript")
74
+ empty_directory "app/javascript/controllers"
75
+ if File.exist?(toast_source)
76
+ copy_file toast_source, "app/javascript/controllers/odt_toast_controller.js"
77
+ say "Copied app/javascript/controllers/odt_toast_controller.js", :green
78
+ end
79
+ if File.exist?(modal_source)
80
+ copy_file modal_source, "app/javascript/controllers/odt_modal_controller.js"
81
+ say "Copied app/javascript/controllers/odt_modal_controller.js", :green
82
+ end
83
+ end
84
+ end
68
85
  end
69
86
  end
70
87
  end
@@ -0,0 +1,61 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["dialog"]
5
+ static values = {
6
+ open: { type: Boolean, default: false },
7
+ preventBackdropClose: { type: Boolean, default: false }
8
+ }
9
+
10
+ connect() {
11
+ if (this.openValue) {
12
+ this.open()
13
+ }
14
+ }
15
+
16
+ open(event) {
17
+ if (event) event.preventDefault()
18
+
19
+ this.element.classList.remove("hidden")
20
+ this.element.setAttribute("aria-hidden", "false")
21
+ this.element.classList.remove("odt-modal-overlay--exiting")
22
+ if (this.hasDialogTarget) {
23
+ this.dialogTarget.classList.remove("odt-modal--exiting")
24
+ }
25
+
26
+ document.body.style.overflow = "hidden"
27
+ this.element.dispatchEvent(new CustomEvent("odt-modal:opened", { bubbles: true }))
28
+ }
29
+
30
+ close(event) {
31
+ if (event) event.preventDefault()
32
+
33
+ this.element.classList.add("odt-modal-overlay--exiting")
34
+ if (this.hasDialogTarget) {
35
+ this.dialogTarget.classList.add("odt-modal--exiting")
36
+ }
37
+
38
+ document.body.style.overflow = ""
39
+
40
+ setTimeout(() => {
41
+ this.element.classList.add("hidden")
42
+ this.element.classList.remove("odt-modal-overlay--exiting")
43
+ if (this.hasDialogTarget) {
44
+ this.dialogTarget.classList.remove("odt-modal--exiting")
45
+ }
46
+ this.element.setAttribute("aria-hidden", "true")
47
+ this.element.dispatchEvent(new CustomEvent("odt-modal:closed", { bubbles: true }))
48
+ }, 200)
49
+ }
50
+
51
+ backdropClose(event) {
52
+ if (this.preventBackdropCloseValue) return
53
+ if (event.target === this.element) {
54
+ this.close(event)
55
+ }
56
+ }
57
+
58
+ disconnect() {
59
+ document.body.style.overflow = ""
60
+ }
61
+ }
@@ -0,0 +1,40 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ duration: { type: Number, default: 4000 }
6
+ }
7
+
8
+ connect() {
9
+ this.startTimer()
10
+ }
11
+
12
+ startTimer() {
13
+ if (this.durationValue > 0) {
14
+ this.timeout = setTimeout(() => this.dismiss(), this.durationValue)
15
+ }
16
+ }
17
+
18
+ pauseTimer() {
19
+ if (this.timeout) {
20
+ clearTimeout(this.timeout)
21
+ this.timeout = null
22
+ }
23
+ }
24
+
25
+ resumeTimer() {
26
+ this.startTimer()
27
+ }
28
+
29
+ dismiss() {
30
+ this.pauseTimer()
31
+ this.element.classList.add("odt-toast--exiting")
32
+ setTimeout(() => {
33
+ this.element.remove()
34
+ }, 250)
35
+ }
36
+
37
+ disconnect() {
38
+ this.pauseTimer()
39
+ }
40
+ }