@rupe/v-datepicker 1.0.0-alpha.1 → 1.0.0-beta.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.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # v-datepicker
2
+
3
+ A flexible and accessible Vue 3 datepicker component library.
4
+
5
+ ## Description
6
+
7
+ `v-datepicker` is a highly customizable datepicker package built for the Vue 3 ecosystem. It is primarily based on the robust implementation of [reka-ui](https://github.com/reka-ui/reka-ui) (formerly radix-vue) to ensure top-tier accessibility and performance.
8
+
9
+ This package is further enriched with advanced features like the **Month and Year Overlay**, inspired by the popular [vue-datepicker](https://github.com/Vuepic/vue-datepicker), providing a more intuitive navigation experience for users.
10
+
11
+ > **Note:** This is a **headless** library. No styling is included out of the box. You have full control over the look and feel, and are responsible for applying all styles (e.g., using Tailwind CSS).
12
+
13
+ ## Features
14
+
15
+ - **Headless & Accessible**: Built on top of Reka UI's accessible primitives.
16
+ - **Customizable**: Full control over the rendering of every part of the datepicker.
17
+ - **Tree-shakable**: Built using modern ES modules.
18
+ - **Month & Year Overlays**: Quick navigation inspired by Vue Datepicker.
19
+ - **TypeScript Ready**: Full type support for a better developer experience.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ # pnpm
25
+ pnpm add @rupe/v-datepicker
26
+
27
+ # npm
28
+ npm install @rupe/v-datepicker
29
+
30
+ # yarn
31
+ yarn add @rupe/v-datepicker
32
+ ```
33
+
34
+ ## Basic Usage (Tailwind CSS)
35
+
36
+ Since the library is headless, you'll need to apply your own styles. Here's an example using Tailwind CSS:
37
+
38
+ ```vue
39
+ <script setup lang="ts">
40
+ import { DatePicker } from '@rupe/v-datepicker'
41
+ </script>
42
+
43
+ <template>
44
+ <DatePicker.Root class="relative">
45
+ <DatePicker.Field v-slot="{ segments }" class="flex items-center gap-1 border border-gray-300 rounded px-3 py-2 bg-white focus-within:ring-2 focus:ring-blue-500">
46
+ <div class="flex items-center">
47
+ <template v-for="item in segments" :key="item.part">
48
+ <DatePicker.Input
49
+ :part="item.part"
50
+ class="px-0.5 rounded focus:bg-blue-100 focus:outline-none data-[placeholder]:text-gray-400"
51
+ >
52
+ {{ item.value }}
53
+ </DatePicker.Input>
54
+ </template>
55
+ </div>
56
+ <DatePicker.Trigger class="ml-auto text-gray-500 hover:text-gray-700">
57
+ 📅
58
+ </DatePicker.Trigger>
59
+ </DatePicker.Field>
60
+
61
+ <DatePicker.Content class="absolute z-10 mt-2 bg-white border border-gray-200 rounded-lg shadow-xl p-4">
62
+ <DatePicker.Calendar v-slot="{ weekDays, grid }">
63
+ <DatePicker.Header class="flex items-center justify-between mb-4">
64
+ <DatePicker.Prev class="p-1 hover:bg-gray-100 rounded">⬅️</DatePicker.Prev>
65
+ <DatePicker.Heading class="font-bold flex gap-1">
66
+ <DatePicker.MonthHeading />
67
+ <DatePicker.YearHeading />
68
+ </DatePicker.Heading>
69
+ <DatePicker.Next class="p-1 hover:bg-gray-100 rounded">➡️</DatePicker.Next>
70
+ </DatePicker.Header>
71
+
72
+ <DatePicker.Grid v-for="month in grid" :key="month.value.toString()" class="w-full">
73
+ <DatePicker.GridHead>
74
+ <DatePicker.GridRow class="flex justify-between">
75
+ <DatePicker.HeadCell v-for="day in weekDays" :key="day" class="w-8 text-center text-xs text-gray-500 font-medium">
76
+ {{ day }}
77
+ </DatePicker.HeadCell>
78
+ </DatePicker.GridRow>
79
+ </DatePicker.GridHead>
80
+ <DatePicker.GridBody>
81
+ <DatePicker.GridRow v-for="(weekDates, index) in month.rows" :key="index" class="flex justify-between mt-1">
82
+ <DatePicker.Cell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
83
+ <DatePicker.CellTrigger
84
+ :day="weekDate"
85
+ :month="month.value"
86
+ class="w-8 h-8 flex items-center justify-center rounded-full text-sm hover:bg-blue-50 data-[selected]:bg-blue-600 data-[selected]:text-white data-[outside-view]:text-gray-300"
87
+ />
88
+ </DatePicker.Cell>
89
+ </DatePicker.GridRow>
90
+ </DatePicker.GridBody>
91
+ </DatePicker.Grid>
92
+ </DatePicker.Calendar>
93
+ </DatePicker.Content>
94
+ </DatePicker.Root>
95
+ </template>
96
+ ```
97
+
98
+ ## Playground
99
+
100
+ The repository includes a playground project built with **Nuxt** and **Tailwind CSS** to test the library locally.
101
+
102
+ ### Running the Playground
103
+
104
+ 1. **Clone the repository**
105
+ 2. **Install dependencies**:
106
+ ```bash
107
+ pnpm install
108
+ ```
109
+ 3. **Start the playground**:
110
+ ```bash
111
+ pnpm dev
112
+ ```
113
+ This will start the Nuxt dev server for the `playground` folder. Any changes made in the `src/` directory will be reflected instantly via HMR.
114
+
115
+ ## Credits
116
+
117
+ Special thanks to the following projects that made this package possible:
118
+
119
+ - **[reka-ui](https://reka-ui.com/)**: For the incredible foundation of accessible headless components.
120
+ - **[vue-datepicker](https://vue3-datepicker.com/)**: For the inspiration behind the intuitive month and year overlay navigation.
121
+
122
+ ## License
123
+
124
+ MIT
@@ -1,26 +1,25 @@
1
+ import { ComputedRef, DefineComponent, ComponentOptionsMixin, PublicProps, ComponentProvideOptions, Ref } from 'vue';
1
2
  import { PrimitiveProps } from '../Primitive';
2
3
  import { MonthNameDateValue } from './CalendarRoot.vue';
3
- import { Ref, DefineComponent, ComponentOptionsMixin, PublicProps, ComponentProvideOptions } from 'vue';
4
4
  export interface CalendarMonthYearOverlayProps extends PrimitiveProps {
5
5
  type: "month" | "year";
6
+ itemsPerRow?: number;
6
7
  }
7
- declare function __VLS_template(): {
8
- attrs: Partial<{}>;
9
- slots: {
10
- default?(_: {
11
- months: MonthNameDateValue[];
12
- years: {
13
- year: number;
14
- }[];
15
- state: Ref<false | "month" | "year", false | "month" | "year">;
16
- }): any;
17
- };
18
- refs: {};
19
- rootEl: any;
8
+ type CalendarMonthYearOverlayContext = {
9
+ itemsPerRow: ComputedRef<number>;
20
10
  };
21
- type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
22
- declare const __VLS_component: DefineComponent<CalendarMonthYearOverlayProps, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<CalendarMonthYearOverlayProps> & Readonly<{}>, {}, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
23
- declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
11
+ export declare const injectCalendarMonthYearOverlayContext: <T extends CalendarMonthYearOverlayContext | null | undefined = CalendarMonthYearOverlayContext>(fallback?: T | undefined) => T extends null ? CalendarMonthYearOverlayContext | null : CalendarMonthYearOverlayContext, provideCalendarMonthYearOverlayContext: (contextValue: CalendarMonthYearOverlayContext) => CalendarMonthYearOverlayContext;
12
+ declare const _default: __VLS_WithTemplateSlots< DefineComponent<CalendarMonthYearOverlayProps, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<CalendarMonthYearOverlayProps> & Readonly<{}>, {
13
+ itemsPerRow: number;
14
+ }, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>, {
15
+ default?(_: {
16
+ months: MonthNameDateValue[][];
17
+ years: {
18
+ year: number;
19
+ }[][];
20
+ state: Ref<false | "month" | "year", false | "month" | "year">;
21
+ }): any;
22
+ }>;
24
23
  export default _default;
25
24
  type __VLS_WithTemplateSlots<T, S> = T & {
26
25
  new (): {
@@ -1,21 +1,99 @@
1
1
  import { DateValue } from '@internationalized/date';
2
2
  import { PrimitiveProps, AsTag } from '../Primitive';
3
- import { DefineComponent, ComponentOptionsMixin, PublicProps, Component, ComponentProvideOptions } from 'vue';
3
+ import { CreateComponentPublicInstanceWithMixins, ExtractPropTypes, PropType, Component, VNode, RendererNode, RendererElement, ComponentOptionsMixin, PublicProps, GlobalComponents, GlobalDirectives, ComponentProvideOptions, DefineComponent } from 'vue';
4
4
  export interface CalendarOverlayItemProps extends PrimitiveProps {
5
- date: DateValue;
5
+ date: DateValue & {
6
+ monthName: string;
7
+ };
8
+ disabled?: boolean;
9
+ type: "month" | "year";
6
10
  }
7
11
  declare function __VLS_template(): {
8
12
  attrs: Partial<{}>;
9
13
  slots: {
10
14
  default?(_: {}): any;
11
15
  };
12
- refs: {};
16
+ refs: {
17
+ primitiveElement: CreateComponentPublicInstanceWithMixins<Readonly< ExtractPropTypes<{
18
+ asChild: {
19
+ type: BooleanConstructor;
20
+ default: boolean;
21
+ };
22
+ as: {
23
+ type: PropType< AsTag | Component>;
24
+ default: string;
25
+ };
26
+ }>> & Readonly<{}>, () => VNode<RendererNode, RendererElement, {
27
+ [key: string]: any;
28
+ }>, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, PublicProps, {
29
+ asChild: boolean;
30
+ as: AsTag | Component;
31
+ }, true, {}, {}, GlobalComponents, GlobalDirectives, string, {}, any, ComponentProvideOptions, {
32
+ P: {};
33
+ B: {};
34
+ D: {};
35
+ C: {};
36
+ M: {};
37
+ Defaults: {};
38
+ }, Readonly< ExtractPropTypes<{
39
+ asChild: {
40
+ type: BooleanConstructor;
41
+ default: boolean;
42
+ };
43
+ as: {
44
+ type: PropType< AsTag | Component>;
45
+ default: string;
46
+ };
47
+ }>> & Readonly<{}>, () => VNode<RendererNode, RendererElement, {
48
+ [key: string]: any;
49
+ }>, {}, {}, {}, {
50
+ asChild: boolean;
51
+ as: AsTag | Component;
52
+ }> | null;
53
+ };
13
54
  rootEl: any;
14
55
  };
15
56
  type __VLS_TemplateResult = ReturnType<typeof __VLS_template>;
16
57
  declare const __VLS_component: DefineComponent<CalendarOverlayItemProps, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, PublicProps, Readonly<CalendarOverlayItemProps> & Readonly<{}>, {
17
58
  as: AsTag | Component;
18
- }, {}, {}, {}, string, ComponentProvideOptions, false, {}, any>;
59
+ }, {}, {}, {}, string, ComponentProvideOptions, false, {
60
+ primitiveElement: CreateComponentPublicInstanceWithMixins<Readonly< ExtractPropTypes<{
61
+ asChild: {
62
+ type: BooleanConstructor;
63
+ default: boolean;
64
+ };
65
+ as: {
66
+ type: PropType< AsTag | Component>;
67
+ default: string;
68
+ };
69
+ }>> & Readonly<{}>, () => VNode<RendererNode, RendererElement, {
70
+ [key: string]: any;
71
+ }>, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, PublicProps, {
72
+ asChild: boolean;
73
+ as: AsTag | Component;
74
+ }, true, {}, {}, GlobalComponents, GlobalDirectives, string, {}, any, ComponentProvideOptions, {
75
+ P: {};
76
+ B: {};
77
+ D: {};
78
+ C: {};
79
+ M: {};
80
+ Defaults: {};
81
+ }, Readonly< ExtractPropTypes<{
82
+ asChild: {
83
+ type: BooleanConstructor;
84
+ default: boolean;
85
+ };
86
+ as: {
87
+ type: PropType< AsTag | Component>;
88
+ default: string;
89
+ };
90
+ }>> & Readonly<{}>, () => VNode<RendererNode, RendererElement, {
91
+ [key: string]: any;
92
+ }>, {}, {}, {}, {
93
+ asChild: boolean;
94
+ as: AsTag | Component;
95
+ }> | null;
96
+ }, any>;
19
97
  declare const _default: __VLS_WithTemplateSlots<typeof __VLS_component, __VLS_TemplateResult["slots"]>;
20
98
  export default _default;
21
99
  type __VLS_WithTemplateSlots<T, S> = T & {
@@ -6,10 +6,10 @@ declare function __VLS_template(): {
6
6
  attrs: Partial<{}>;
7
7
  slots: {
8
8
  default?(_: {
9
- months: MonthNameDateValue[];
9
+ months: MonthNameDateValue[][];
10
10
  years: {
11
11
  year: number;
12
- }[];
12
+ }[][];
13
13
  }): any;
14
14
  };
15
15
  refs: {};
package/dist/index.cjs CHANGED
@@ -24,6 +24,12 @@ function _interopNamespaceDefault(e) {
24
24
  return Object.freeze(n);
25
25
  }
26
26
  const vue__namespace = /* @__PURE__ */ _interopNamespaceDefault(vue);
27
+ function chunk$1(arr, size) {
28
+ const result = [];
29
+ for (let i = 0; i < arr.length; i += size)
30
+ result.push(arr.slice(i, i + size));
31
+ return result;
32
+ }
27
33
  function createContext(providerComponentName, contextName) {
28
34
  const symbolDescription = typeof providerComponentName === "string" && !contextName ? `${providerComponentName}Context` : contextName;
29
35
  const injectionKey = Symbol(symbolDescription);
@@ -4157,6 +4163,10 @@ const _sfc_main$r = /* @__PURE__ */ vue.defineComponent({
4157
4163
  };
4158
4164
  }
4159
4165
  });
4166
+ const [
4167
+ injectCalendarMonthYearOverlayContext,
4168
+ provideCalendarMonthYearOverlayContext
4169
+ ] = createContext("CalendarMonthYearOverlay");
4160
4170
  const _sfc_main$q = /* @__PURE__ */ vue.defineComponent({
4161
4171
  ...{
4162
4172
  inheritAttrs: false
@@ -4164,25 +4174,35 @@ const _sfc_main$q = /* @__PURE__ */ vue.defineComponent({
4164
4174
  __name: "CalendarMonthYearOverlay",
4165
4175
  props: {
4166
4176
  type: {},
4177
+ itemsPerRow: { default: 4 },
4167
4178
  asChild: { type: Boolean },
4168
4179
  as: {}
4169
4180
  },
4170
4181
  setup(__props) {
4171
4182
  const props = __props;
4183
+ const itemsPerRow = vue.computed(() => props.itemsPerRow);
4172
4184
  const rootContext = injectCalendarRootContext();
4173
- const months = vue.computed(() => rootContext.months.value);
4174
- const years = vue.computed(() => rootContext.years.value);
4175
- const isOpen = vue.computed(() => rootContext.monthYearOverlayState.value === props.type);
4185
+ const months = vue.computed(
4186
+ () => chunk$1(rootContext.months.value, props.itemsPerRow)
4187
+ );
4188
+ const years = vue.computed(() => chunk$1(rootContext.years.value, props.itemsPerRow));
4189
+ const isOpen = vue.computed(
4190
+ () => rootContext.monthYearOverlayState.value === props.type
4191
+ );
4176
4192
  function onEscapeKeyDown() {
4177
4193
  rootContext.monthYearOverlayState.value = false;
4178
4194
  }
4195
+ provideCalendarMonthYearOverlayContext({
4196
+ itemsPerRow
4197
+ });
4179
4198
  return (_ctx, _cache) => {
4180
- return vue.openBlock(), vue.createBlock(_sfc_main$O, {
4199
+ return isOpen.value ? (vue.openBlock(), vue.createBlock(_sfc_main$O, {
4200
+ key: 0,
4181
4201
  "as-child": "",
4182
4202
  onEscapeKeyDown
4183
4203
  }, {
4184
4204
  default: vue.withCtx(() => [
4185
- isOpen.value ? (vue.openBlock(), vue.createBlock(vue.unref(Primitive), vue.mergeProps({ key: 0 }, { ...props, ..._ctx.$attrs }, {
4205
+ vue.createVNode(vue.unref(Primitive), vue.mergeProps({ ...props, ..._ctx.$attrs }, {
4186
4206
  style: {
4187
4207
  position: "absolute",
4188
4208
  height: "100%",
@@ -4202,10 +4222,10 @@ const _sfc_main$q = /* @__PURE__ */ vue.defineComponent({
4202
4222
  })
4203
4223
  ]),
4204
4224
  _: 3
4205
- }, 16)) : vue.createCommentVNode("", true)
4225
+ }, 16)
4206
4226
  ]),
4207
4227
  _: 3
4208
- });
4228
+ })) : vue.createCommentVNode("", true);
4209
4229
  };
4210
4230
  }
4211
4231
  });
@@ -5140,6 +5160,7 @@ const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({
5140
5160
  __name: "DatePickerMonthYearOverlay",
5141
5161
  props: {
5142
5162
  type: {},
5163
+ itemsPerRow: {},
5143
5164
  asChild: { type: Boolean },
5144
5165
  as: {}
5145
5166
  },
@@ -5162,25 +5183,93 @@ const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({
5162
5183
  __name: "CalendarOverlayItem",
5163
5184
  props: {
5164
5185
  date: {},
5186
+ disabled: { type: Boolean },
5187
+ type: {},
5165
5188
  asChild: { type: Boolean },
5166
- as: { default: "button" }
5189
+ as: { default: "div" }
5167
5190
  },
5168
5191
  setup(__props) {
5169
5192
  const props = __props;
5170
5193
  const rootContext = injectCalendarRootContext();
5194
+ const years = vue.computed(() => rootContext.years.value);
5195
+ const maxValue = vue.computed(
5196
+ () => props.type === "month" ? 12 : years.value.length
5197
+ );
5198
+ const overlayContext = injectCalendarMonthYearOverlayContext();
5199
+ const dataValue = vue.computed(() => `${props.type}-${props.date[props.type]}`);
5200
+ const isFocusedDate = vue.computed(() => {
5201
+ if (props.type === "month")
5202
+ return rootContext.currentMonth.value === props.date.monthName;
5203
+ return rootContext.currentYear.value === props.date.year.toString();
5204
+ });
5205
+ function closeOverlay() {
5206
+ rootContext.monthYearOverlayState.value = false;
5207
+ }
5171
5208
  function handleClick() {
5172
5209
  if (rootContext.isDateDisabled(props.date) || rootContext.isDateUnavailable?.(props.date))
5173
5210
  return;
5174
5211
  rootContext.onDateChange(props.date);
5175
- rootContext.monthYearOverlayState.value = false;
5212
+ closeOverlay();
5213
+ }
5214
+ const kbd = useKbd();
5215
+ function handleArrowKey(e) {
5216
+ if (props.disabled) return;
5217
+ e.preventDefault();
5218
+ e.stopPropagation();
5219
+ const parentElement = rootContext.parentElement.value;
5220
+ const indexIncrementation = overlayContext.itemsPerRow.value;
5221
+ const sign = rootContext.dir.value === "rtl" ? -1 : 1;
5222
+ switch (e.code) {
5223
+ case kbd.ARROW_RIGHT:
5224
+ shiftFocus(props.date, sign);
5225
+ break;
5226
+ case kbd.ARROW_LEFT:
5227
+ shiftFocus(props.date, -sign);
5228
+ break;
5229
+ case kbd.ARROW_UP:
5230
+ shiftFocus(props.date, -indexIncrementation);
5231
+ break;
5232
+ case kbd.ARROW_DOWN:
5233
+ shiftFocus(props.date, indexIncrementation);
5234
+ break;
5235
+ case kbd.ENTER:
5236
+ case kbd.SPACE_CODE:
5237
+ rootContext.onDateChange(props.date);
5238
+ closeOverlay();
5239
+ }
5240
+ function shiftFocus(date2, add) {
5241
+ let nextDate = date2[props.type] + add;
5242
+ if (nextDate <= 0) {
5243
+ nextDate = maxValue.value + nextDate;
5244
+ }
5245
+ if (nextDate > years.value[maxValue.value - 1].year) {
5246
+ nextDate = nextDate - maxValue.value;
5247
+ }
5248
+ const candidateDay = parentElement.querySelector(
5249
+ `[data-value='${props.type}-${nextDate}']`
5250
+ );
5251
+ candidateDay?.focus();
5252
+ }
5176
5253
  }
5177
5254
  return (_ctx, _cache) => {
5178
- return vue.openBlock(), vue.createBlock(vue.unref(Primitive), vue.mergeProps(props, { onClick: handleClick }), {
5255
+ return vue.openBlock(), vue.createBlock(vue.unref(Primitive), vue.mergeProps(props, {
5256
+ onClick: handleClick,
5257
+ ref: "primitiveElement",
5258
+ role: "button",
5259
+ "aria-disabled": __props.disabled,
5260
+ "data-value": dataValue.value,
5261
+ onKeydown: [
5262
+ vue.withKeys(handleArrowKey, ["up", "down", "left", "right", "space", "enter"]),
5263
+ _cache[0] || (_cache[0] = vue.withKeys(vue.withModifiers(() => {
5264
+ }, ["prevent"]), ["enter"]))
5265
+ ],
5266
+ tabindex: isFocusedDate.value ? 0 : -1
5267
+ }), {
5179
5268
  default: vue.withCtx(() => [
5180
5269
  vue.renderSlot(_ctx.$slots, "default")
5181
5270
  ]),
5182
5271
  _: 3
5183
- }, 16);
5272
+ }, 16, ["aria-disabled", "data-value", "tabindex"]);
5184
5273
  };
5185
5274
  }
5186
5275
  });
@@ -5188,6 +5277,8 @@ const _sfc_main = /* @__PURE__ */ vue.defineComponent({
5188
5277
  __name: "DatePickerOverlayItem",
5189
5278
  props: {
5190
5279
  date: {},
5280
+ disabled: { type: Boolean },
5281
+ type: {},
5191
5282
  asChild: { type: Boolean },
5192
5283
  as: {}
5193
5284
  },