@reservi/theme 2.0.0-beta.0 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reservi/theme",
3
- "version": "2.0.0-beta.0",
3
+ "version": "2.0.0-beta.2",
4
4
  "description": "Tema de Reservi Calendar: design tokens (TailwindCSS 4 @theme) y estilos base.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,12 +25,15 @@
25
25
  "*.css"
26
26
  ],
27
27
  "files": [
28
- "src"
28
+ "src",
29
+ "scripts"
29
30
  ],
30
31
  "main": "./src/index.css",
31
32
  "style": "./src/index.css",
32
33
  "exports": {
33
34
  ".": "./src/index.css",
35
+ "./plain": "./src/plain.css",
36
+ "./tokens": "./src/tokens.generated.css",
34
37
  "./theme.css": "./src/theme.css",
35
38
  "./components.css": "./src/components.css",
36
39
  "./template": "./src/theme-template.css",
@@ -49,10 +52,11 @@
49
52
  "./themes/graphite": "./src/themes/graphite.css",
50
53
  "./themes/solarized-light": "./src/themes/solarized-light.css",
51
54
  "./themes/solarized-dark": "./src/themes/solarized-dark.css",
52
- "./themes/contrast": "./src/themes/contrast.css"
55
+ "./themes/contrast": "./src/themes/contrast.css",
56
+ "./themes/reservi": "./src/themes/reservi.css"
53
57
  },
54
58
  "scripts": {
55
- "build": "node -e \"process.exit(0)\"",
59
+ "build": "node scripts/build-plain.mjs",
56
60
  "lint": "node -e \"process.exit(0)\"",
57
61
  "typecheck": "node -e \"process.exit(0)\""
58
62
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Genera la variante "plain CSS" del tema a partir de la fuente de TailwindCSS 4.
3
+ *
4
+ * `theme.css` declara los design tokens en un bloque `@theme`, que solo
5
+ * entiende Tailwind 4. Los estilos de `components.css` no usan utilidades de
6
+ * Tailwind: leen las variables con `var(--color-rsv-*, fallback)`. Por tanto,
7
+ * para un consumidor con Tailwind 3 (o sin Tailwind) basta con declarar los
8
+ * mismos tokens en `:root`.
9
+ *
10
+ * Este script hace exactamente esa transformación, de modo que NO haya dos
11
+ * listas de tokens que mantener sincronizadas a mano.
12
+ *
13
+ * Salida: `src/tokens.generated.css`, que importa `plain.css`.
14
+ */
15
+ import { readFile, writeFile } from "node:fs/promises";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, join } from "node:path";
18
+
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+ const src = join(here, "..", "src");
21
+
22
+ const HEADER = `/**
23
+ * ARCHIVO GENERADO — no editar a mano.
24
+ * Fuente: src/theme.css · Generador: scripts/build-plain.mjs
25
+ *
26
+ * Misma lista de design tokens que el bloque \`@theme\` de Tailwind 4, pero
27
+ * declarados en \`:root\` para consumidores con Tailwind 3 o sin Tailwind.
28
+ */
29
+ `;
30
+
31
+ const theme = await readFile(join(src, "theme.css"), "utf8");
32
+
33
+ // Se descarta el comentario de cabecera original (habla de Tailwind 4) antes de
34
+ // comprobar que no queda ningun `@theme`: el comentario lo menciona.
35
+ let out = theme.replace(/^\/\*\*[\s\S]*?\*\/\s*/, "");
36
+
37
+ // `@theme { … }` → `:root { … }`. El bloque puede venir anidado dentro de un
38
+ // `@media (prefers-color-scheme: dark)`, que se conserva tal cual.
39
+ out = out.replace(/@theme(\s+(?:static|inline|reference|default))?\s*\{/g, ":root {");
40
+
41
+ if (out.includes("@theme")) {
42
+ throw new Error("build-plain: queda algun @theme sin transformar en theme.css");
43
+ }
44
+ if (!out.includes(":root {")) {
45
+ throw new Error("build-plain: no se encontro ningun bloque @theme en theme.css");
46
+ }
47
+
48
+ await writeFile(join(src, "tokens.generated.css"), HEADER + "\n" + out, "utf8");
49
+ console.log("@reservi/theme: src/tokens.generated.css generado desde src/theme.css");
@@ -16,6 +16,25 @@
16
16
  flex-direction: column;
17
17
  }
18
18
 
19
+ /* El area de vista reparte el alto disponible del root (flex column), de modo
20
+ que el cuerpo de timeGrid pueda scrollear cuando el root tiene alto fijo.
21
+ Con `height: "auto"` el calendario crece con su contenido. */
22
+ [data-rsv="view"] {
23
+ display: flex;
24
+ flex-direction: column;
25
+ flex: 1 1 auto;
26
+ min-height: 0;
27
+ }
28
+ [data-rsv="root"][data-rsv-autoheight="true"] [data-rsv="view"] {
29
+ flex: 0 0 auto;
30
+ }
31
+
32
+ /* Hueco donde el framework anfitrion (React/Vue) pinta un render prop: no debe
33
+ introducir una caja propia que rompa el layout del evento o de la celda. */
34
+ [data-rsv="host"] {
35
+ display: contents;
36
+ }
37
+
19
38
  /* Toolbar */
20
39
  [data-rsv="toolbar-wrap"] {
21
40
  display: flex;
@@ -348,6 +367,12 @@
348
367
  outline: 2px solid var(--color-rsv-primary, #4f46e5);
349
368
  outline-offset: 1px;
350
369
  }
370
+ /* Selección múltiple (bulk ops): ctrl/cmd-click sobre un evento. */
371
+ [data-rsv="event"][data-rsv-selected="true"] {
372
+ outline: 2px solid var(--color-rsv-primary, #4f46e5);
373
+ outline-offset: 1px;
374
+ box-shadow: 0 0 0 3px var(--color-rsv-selection, #e8e8ff);
375
+ }
351
376
  [data-rsv="day-number"] {
352
377
  align-self: flex-end;
353
378
  font-size: var(--text-rsv-sm, 0.8125rem);
@@ -373,6 +398,7 @@
373
398
  border-radius: var(--radius-rsv-event, 0.375rem);
374
399
  background: var(--rsv-event-bg, var(--color-rsv-event-1));
375
400
  color: var(--rsv-event-fg, var(--color-rsv-event-fg, #fff));
401
+ border: 1px solid var(--rsv-event-border, transparent);
376
402
  cursor: pointer;
377
403
  overflow: hidden;
378
404
  white-space: nowrap;
@@ -475,6 +501,8 @@
475
501
  [data-rsv="timegrid"] {
476
502
  display: flex;
477
503
  flex-direction: column;
504
+ flex: 1 1 auto;
505
+ min-height: 0;
478
506
  --rsv-tg-axis-width: 3.5rem;
479
507
  }
480
508
  [data-rsv="tg-header"],
@@ -511,6 +539,11 @@
511
539
  display: grid;
512
540
  grid-template-columns: var(--rsv-tg-axis-width) repeat(var(--rsv-tg-days, 7), minmax(0, 1fr));
513
541
  overflow: auto;
542
+ /* Imprescindible para que un grid dentro de un flex column pueda encogerse
543
+ y por tanto scrollear en vez de desbordar al padre. */
544
+ flex: 1 1 auto;
545
+ min-height: 0;
546
+ align-content: start;
514
547
  }
515
548
  [data-rsv="tg-axis"] {
516
549
  display: flex;
@@ -524,13 +557,25 @@
524
557
  [data-rsv="tg-slot"][data-major="true"] {
525
558
  border-top: 1px solid var(--color-rsv-border, #eee);
526
559
  }
560
+ /**
561
+ * La etiqueta va centrada en su franja, no montada sobre la línea superior.
562
+ *
563
+ * Antes se subía con `top: -0.6em` y se le pintaba detrás un rectángulo opaco
564
+ * de `--color-rsv-bg` para tapar la línea que cruzaba el texto. Ese rectángulo
565
+ * asumía que el fondo real del eje era exactamente `--color-rsv-bg`, y se veía
566
+ * como una mancha en cuanto algo lo teñía (día actual, `surface`, franjas no
567
+ * laborables, o un token sobreescrito por la app).
568
+ *
569
+ * Centrada no cruza ningún borde, así que el rectángulo sobra y el problema
570
+ * desaparece en vez de taparse.
571
+ */
527
572
  [data-rsv="tg-slot-label"] {
528
573
  position: absolute;
529
- top: -0.6em;
574
+ top: 50%;
575
+ transform: translateY(-50%);
530
576
  right: 0.35rem;
531
577
  font-size: var(--text-rsv-xs, 0.75rem);
532
578
  color: var(--color-rsv-muted, #777);
533
- background: var(--color-rsv-bg, #fff);
534
579
  }
535
580
  [data-rsv="tg-col"] {
536
581
  position: relative;
@@ -554,7 +599,72 @@
554
599
  font-size: var(--text-rsv-xs, 0.75rem);
555
600
  overflow: hidden;
556
601
  box-sizing: border-box;
557
- border: 1px solid color-mix(in oklch, var(--rsv-event-bg) 80%, black);
602
+ /* `borderColor` del evento si lo hay; si no, un tono del propio fondo. */
603
+ border: 1px solid var(--rsv-event-border, color-mix(in oklch, var(--rsv-event-bg) 80%, black));
604
+ align-items: stretch;
605
+ white-space: normal;
606
+ }
607
+
608
+ /**
609
+ * Arrastre de un evento en timeGrid.
610
+ *
611
+ * El `cursor: grab` solo existia para daygrid, asi que en las vistas de tiempo
612
+ * nada indicaba que la tarjeta se pudiera mover. Y durante el gesto no se
613
+ * pintaba nada: la tarjeta se quedaba quieta y saltaba a su nueva posicion solo
614
+ * al soltar. `drag-mirror` muestra donde va a caer, y la tarjeta de origen se
615
+ * atenua para leerse como "levantada".
616
+ */
617
+ [data-rsv="tg-col"] [data-rsv="event"][data-editable="true"],
618
+ [data-rsv="tg-col"] [data-rsv="event"]:has([data-rsv="resize-handle"]) {
619
+ cursor: grab;
620
+ touch-action: none;
621
+ }
622
+ [data-rsv="event"][data-rsv-dragging="true"] {
623
+ cursor: grabbing;
624
+ opacity: 0.55;
625
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.22);
626
+ z-index: 30;
627
+ }
628
+
629
+ /**
630
+ * Mes: el destino es una celda de dia entera, no una franja horaria, asi que
631
+ * en vez de un fantasma se resalta el dia bajo el puntero. La tarjeta de origen
632
+ * se eleva con la misma regla `[data-rsv-dragging]` que en timeGrid.
633
+ */
634
+ [data-rsv="day"][data-rsv-drop="true"] {
635
+ outline: 2px dashed var(--color-rsv-primary, #4f46e5);
636
+ outline-offset: -2px;
637
+ background: var(--color-rsv-selection, #e8e8ff);
638
+ }
639
+ [data-rsv="daygrid"] [data-rsv="event"][data-rsv-dragging="true"] {
640
+ cursor: grabbing;
641
+ opacity: 0.55;
642
+ }
643
+
644
+ /* Fantasma que sigue al puntero mientras se arrastra un evento. */
645
+ [data-rsv="drag-mirror"] {
646
+ position: absolute;
647
+ left: 0;
648
+ right: 0;
649
+ pointer-events: none;
650
+ z-index: 25;
651
+ border: 1px dashed var(--color-rsv-primary, #4f46e5);
652
+ border-radius: var(--radius-rsv-event, 0.375rem);
653
+ background: var(--color-rsv-selection, #e8e8ff);
654
+ opacity: 0.85;
655
+ }
656
+
657
+ /* Fantasma del rango que se esta seleccionando (`selectMirror`). */
658
+ [data-rsv="select-mirror"] {
659
+ position: absolute;
660
+ left: 0;
661
+ right: 0;
662
+ background: var(--color-rsv-selection, #e8e8ff);
663
+ border: 1px dashed var(--color-rsv-primary, #4f46e5);
664
+ border-radius: var(--radius-rsv-event, 0.375rem);
665
+ opacity: 0.7;
666
+ pointer-events: none;
667
+ z-index: 4;
558
668
  }
559
669
  [data-rsv="tg-nonbusiness"] {
560
670
  position: absolute;
@@ -564,6 +674,12 @@
564
674
  opacity: 0.6;
565
675
  pointer-events: none;
566
676
  }
677
+ /* La banda no intercepta el puntero —el click tiene que llegar a la columna para
678
+ la parte abierta del día—, así que el cursor se pone en la columna cuando el
679
+ día no abre en absoluto. */
680
+ [data-rsv="tg-col"][data-business="false"] {
681
+ cursor: not-allowed;
682
+ }
567
683
  [data-rsv="resize-handle"] {
568
684
  position: absolute;
569
685
  left: 0;
@@ -602,6 +718,10 @@
602
718
  flex-direction: column;
603
719
  }
604
720
  [data-rsv="list-day-header"] {
721
+ display: flex;
722
+ align-items: baseline;
723
+ justify-content: space-between;
724
+ gap: 0.5rem;
605
725
  position: sticky;
606
726
  top: 0;
607
727
  padding: 0.4rem 0.75rem;
@@ -615,6 +735,11 @@
615
735
  [data-rsv="list-day"][data-today="true"] [data-rsv="list-day-header"] {
616
736
  color: var(--color-rsv-primary, #4f46e5);
617
737
  }
738
+ [data-rsv="list-day-side-text"] {
739
+ font-weight: 400;
740
+ font-size: var(--text-rsv-xs, 0.75rem);
741
+ color: var(--color-rsv-muted, #777);
742
+ }
618
743
  [data-rsv="list-item"] {
619
744
  display: flex;
620
745
  align-items: center;
@@ -812,6 +937,55 @@
812
937
  cursor: pointer;
813
938
  }
814
939
 
940
+ /* Vista mensual con recursos como columnas anidadas por día. */
941
+ [data-rsv="resource-daygrid-month"] {
942
+ display: flex;
943
+ flex-direction: column;
944
+ }
945
+ [data-rsv="rdgm-week"] {
946
+ display: grid;
947
+ grid-template-columns: repeat(var(--rsv-rdgm-cols, 7), minmax(0, 1fr));
948
+ }
949
+ [data-rsv="rdgm-week"] [data-rsv="day"]:last-child {
950
+ border-right: none;
951
+ }
952
+ [data-rsv="rdgm-columns"] {
953
+ display: flex;
954
+ flex-direction: column;
955
+ gap: 0.2rem;
956
+ overflow: hidden;
957
+ }
958
+ [data-rsv="rdgm-col"] {
959
+ display: flex;
960
+ flex-direction: column;
961
+ gap: 0.1rem;
962
+ border-inline-start: 2px solid var(--color-rsv-border, #eee);
963
+ padding-inline-start: 0.2rem;
964
+ }
965
+ [data-rsv="rdgm-col-label"] {
966
+ font-size: 0.65rem;
967
+ font-weight: 600;
968
+ color: var(--color-rsv-muted, #777);
969
+ text-transform: uppercase;
970
+ letter-spacing: 0.02em;
971
+ }
972
+ [data-rsv="rdgm-col"] [data-rsv="event"] {
973
+ background: var(--rsv-event-bg, var(--color-rsv-event-1));
974
+ color: var(--rsv-event-fg, #fff);
975
+ border-radius: var(--radius-rsv-event, 0.375rem);
976
+ padding: 0.05rem 0.3rem;
977
+ font-size: var(--text-rsv-xs, 0.75rem);
978
+ cursor: pointer;
979
+ overflow: hidden;
980
+ white-space: nowrap;
981
+ text-overflow: ellipsis;
982
+ }
983
+ [data-rsv="rdgm-more"] {
984
+ font-size: 0.65rem;
985
+ color: var(--color-rsv-muted, #777);
986
+ cursor: pointer;
987
+ }
988
+
815
989
  /* Eventos de fondo (display: background) */
816
990
  [data-rsv="event"][data-display="background"] {
817
991
  opacity: 0.25;
package/src/plain.css ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Punto de entrada "plain CSS" de Reservi Calendar: sin Tailwind.
3
+ *
4
+ * Uso (Tailwind 3, CSS Modules, PostCSS o un <link> suelto):
5
+ * @import "@reservi/theme/plain";
6
+ *
7
+ * Equivale a `@reservi/theme` pero declara los design tokens en `:root` en
8
+ * lugar de en un bloque `@theme` de TailwindCSS 4. Los estilos de componentes
9
+ * son los mismos: leen los tokens con `var(--color-rsv-*, fallback)`.
10
+ */
11
+ @import "./tokens.generated.css";
12
+ @import "./components.css";
@@ -18,3 +18,4 @@
18
18
  @import "./solarized-light.css";
19
19
  @import "./solarized-dark.css";
20
20
  @import "./contrast.css";
21
+ @import "./reservi.css";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Theme "reservi" — claro, paleta de marca de Reservi.
3
+ *
4
+ * Los valores son los mismos hex de `tailwind.config.js` de reservi-frontend-v2.
5
+ * Se usa hex y no `oklch()` como el resto de themes a propósito: son colores de
6
+ * marca y la conversión introduce desviación; aquí pesa más la fidelidad.
7
+ *
8
+ * `--font-rsv-sans` NO se declara aquí: la fuente de la app (Urbanist) llega
9
+ * como variable de Next, así que la aporta el frontend vía `themeTokens`.
10
+ */
11
+ [data-rsv-theme="reservi"] {
12
+ /* Superficies */
13
+ --color-rsv-bg: #fefefe;
14
+ --color-rsv-surface: #faf5fc;
15
+ --color-rsv-border: #efefef;
16
+ --color-rsv-muted: #5e636b;
17
+ --color-rsv-text: #1a1a1a;
18
+
19
+ /* Marca / acento */
20
+ --color-rsv-primary: #865e9b;
21
+ --color-rsv-primary-fg: #fefefe;
22
+ /* Translúcido a propósito: tiñe la columna del día actual sin tapar las
23
+ líneas de franja que se pintan debajo. */
24
+ --color-rsv-today: rgba(155, 135, 245, 0.1);
25
+
26
+ /* Eventos — mapeados a los estados de cita (`border-status-*`). */
27
+ --color-rsv-event-1: #865e9b; /* pendiente */
28
+ --color-rsv-event-2: #78b157; /* confirmada */
29
+ --color-rsv-event-3: #e1d25e; /* no-show */
30
+ --color-rsv-event-4: #d85353; /* cancelada */
31
+ --color-rsv-event-5: #bbbec2; /* completada */
32
+ /* Texto oscuro: la paleta de estados va de claro (#e1d25e, #bbbec2) a medio,
33
+ y un fg blanco sería ilegible sobre los dos claros. */
34
+ --color-rsv-event-fg: #141517;
35
+
36
+ /* Estados */
37
+ --color-rsv-now: #d85353;
38
+ --color-rsv-selection: #f6f0fa;
39
+ /* Gris con cuerpo, no un blanco roto: estas franjas tienen que leerse como
40
+ "aqui no se puede", no como un matiz del fondo. */
41
+ --color-rsv-nonbusiness: #f1f2f4;
42
+
43
+ /* Geometría */
44
+ --radius-rsv: 8px;
45
+ --radius-rsv-event: 4px;
46
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ARCHIVO GENERADO — no editar a mano.
3
+ * Fuente: src/theme.css · Generador: scripts/build-plain.mjs
4
+ *
5
+ * Misma lista de design tokens que el bloque `@theme` de Tailwind 4, pero
6
+ * declarados en `:root` para consumidores con Tailwind 3 o sin Tailwind.
7
+ */
8
+
9
+ :root {
10
+ /* Superficies */
11
+ --color-rsv-bg: oklch(1 0 0);
12
+ --color-rsv-surface: oklch(0.98 0 0);
13
+ --color-rsv-border: oklch(0.9 0 0);
14
+ --color-rsv-muted: oklch(0.55 0 0);
15
+ --color-rsv-text: oklch(0.2 0 0);
16
+
17
+ /* Marca / acento */
18
+ --color-rsv-primary: oklch(0.55 0.2 264);
19
+ --color-rsv-primary-fg: oklch(0.99 0 0);
20
+ --color-rsv-today: oklch(0.97 0.04 264);
21
+
22
+ /* Paleta categórica de eventos */
23
+ --color-rsv-event-1: oklch(0.6 0.18 264);
24
+ --color-rsv-event-2: oklch(0.65 0.17 150);
25
+ --color-rsv-event-3: oklch(0.7 0.17 60);
26
+ --color-rsv-event-4: oklch(0.62 0.2 20);
27
+ --color-rsv-event-5: oklch(0.6 0.16 320);
28
+ --color-rsv-event-fg: oklch(0.99 0 0);
29
+
30
+ /* Estados */
31
+ --color-rsv-now: oklch(0.6 0.22 20);
32
+ --color-rsv-selection: oklch(0.9 0.05 264);
33
+ --color-rsv-nonbusiness: oklch(0.96 0 0);
34
+
35
+ /* Tipografía */
36
+ --font-rsv-sans: ui-sans-serif, system-ui, sans-serif;
37
+ --text-rsv-xs: 0.75rem;
38
+ --text-rsv-sm: 0.8125rem;
39
+ --text-rsv-base: 0.875rem;
40
+
41
+ /* Geometría */
42
+ --radius-rsv: 0.5rem;
43
+ --radius-rsv-event: 0.375rem;
44
+ --spacing-rsv-slot: 2.5rem;
45
+ --spacing-rsv-gutter: 0.25rem;
46
+
47
+ /* Sombras y transición */
48
+ --shadow-rsv-pop: 0 4px 20px -4px oklch(0 0 0 / 0.2);
49
+ --ease-rsv: cubic-bezier(0.2, 0, 0, 1);
50
+ }
51
+
52
+ /* Modo oscuro automático */
53
+ @media (prefers-color-scheme: dark) {
54
+ :root {
55
+ --color-rsv-bg: oklch(0.18 0.01 264);
56
+ --color-rsv-surface: oklch(0.22 0.01 264);
57
+ --color-rsv-border: oklch(0.3 0.01 264);
58
+ --color-rsv-text: oklch(0.95 0 0);
59
+ --color-rsv-muted: oklch(0.6 0 0);
60
+ --color-rsv-today: oklch(0.3 0.05 264);
61
+ --color-rsv-nonbusiness: oklch(0.2 0.01 264);
62
+ }
63
+ }
64
+
65
+ /* Modo oscuro por clase (.dark) para toggle manual */
66
+ .dark {
67
+ --color-rsv-bg: oklch(0.18 0.01 264);
68
+ --color-rsv-surface: oklch(0.22 0.01 264);
69
+ --color-rsv-border: oklch(0.3 0.01 264);
70
+ --color-rsv-text: oklch(0.95 0 0);
71
+ --color-rsv-muted: oklch(0.6 0 0);
72
+ --color-rsv-today: oklch(0.3 0.05 264);
73
+ --color-rsv-nonbusiness: oklch(0.2 0.01 264);
74
+ }