@digitalwalletcorp/vue-core 0.3.0 → 1.0.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 (37) hide show
  1. package/README.md +29 -0
  2. package/lib/components-CvmuAs2C.cjs +1 -0
  3. package/lib/components-p1BluzXb.js +18 -0
  4. package/lib/components.d.ts +3 -0
  5. package/lib/hourglass-loading.cjs +1 -0
  6. package/lib/hourglass-loading.d.ts +23 -0
  7. package/lib/hourglass-loading.js +4 -0
  8. package/lib/hourglass-loading.vue_vue_type_style_index_0_lang-C72UVA9W.cjs +1 -0
  9. package/lib/hourglass-loading.vue_vue_type_style_index_0_lang-DkUGIxQ2.js +22 -0
  10. package/lib/index.cjs +1 -1
  11. package/lib/index.d.ts +1 -0
  12. package/lib/index.js +12 -6
  13. package/lib/internal/tab-group-context.d.ts +16 -0
  14. package/lib/nuxt.cjs +1 -1
  15. package/lib/nuxt.js +1 -1
  16. package/lib/register.cjs +1 -1
  17. package/lib/register.js +1 -1
  18. package/lib/style.css +1 -1
  19. package/lib/tab-content.cjs +1 -0
  20. package/lib/tab-content.d.ts +39 -0
  21. package/lib/tab-content.js +4 -0
  22. package/lib/tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs +1 -0
  23. package/lib/tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js +44 -0
  24. package/lib/tab-group.cjs +1 -0
  25. package/lib/tab-group.d.ts +44 -0
  26. package/lib/tab-group.js +4 -0
  27. package/lib/tab-group.vue_vue_type_style_index_0_lang-CjnBkvvZ.js +103 -0
  28. package/lib/tab-group.vue_vue_type_style_index_0_lang-CwVGYZAo.cjs +1 -0
  29. package/package.json +1 -1
  30. package/src/components.ts +3 -0
  31. package/src/hourglass-loading.vue +42 -0
  32. package/src/index.ts +1 -0
  33. package/src/internal/tab-group-context.ts +19 -0
  34. package/src/tab-content.vue +147 -0
  35. package/src/tab-group.vue +246 -0
  36. package/lib/components-B3xZ0gFq.js +0 -12
  37. package/lib/components-CrmajA9z.cjs +0 -1
package/README.md CHANGED
@@ -93,13 +93,42 @@ export default defineNuxtConfig({
93
93
  >
94
94
  > Components such as `AccordionFieldset` rely on [@digitalwalletcorp/vue-svg-icons](https://www.npmjs.com/package/@digitalwalletcorp/vue-svg-icons). Importing its stylesheet ensures icons align properly with text.
95
95
 
96
+ ##### ⚙️ Adapting to your application
97
+
98
+ The components do not know your application's theme. Colors are referenced through `--dwui-*` CSS variables with fallbacks, and default styles have zero specificity (`:where()`).
99
+ We recommend keeping a `vue-core-variables.css` in your application that connects your application's variables to the `--dwui-*` variables, and loading it after the stylesheets above. The variables of each component are listed in its document.
100
+
101
+ ```css
102
+ /* vue-core-variables.css */
103
+ :root {
104
+ --dwui-tab-header-bg: var(--app-tab-header-background);
105
+ --dwui-tab-header-text: var(--app-tab-header-text);
106
+ --dwui-tab-header-selected-bg: var(--app-tab-header-selected-background);
107
+ --dwui-tab-header-selected-text: var(--app-tab-header-selected-text);
108
+ }
109
+ ```
110
+
111
+ ```ts
112
+ // nuxt.config.ts
113
+ export default defineNuxtConfig({
114
+ css: [
115
+ '@digitalwalletcorp/vue-core/style.css',
116
+ '@digitalwalletcorp/vue-svg-icons/style.css',
117
+ '@/assets/css/default.css', // your global CSS
118
+ '@/assets/css/vue-core-variables.css'
119
+ ]
120
+ });
121
+ ```
122
+
96
123
  #### 🧰 Components
97
124
 
98
125
  | Component | Description |
99
126
  | --------- | ----------- |
100
127
  | [`AccordionFieldset`](https://github.com/digitalwalletcorp/vue-core/blob/main/docs/components/accordion-fieldset.md) | A fieldset whose contents can be collapsed and expanded from the legend. |
128
+ | [`HourglassLoading`](https://github.com/digitalwalletcorp/vue-core/blob/main/docs/components/hourglass-loading.md) | Shows an hourglass in place of a value while it is loading. |
101
129
  | [`LabelIcon`](https://github.com/digitalwalletcorp/vue-core/blob/main/docs/components/label-icon.md) | Displays an icon and a text label side by side. |
102
130
  | [`LabelButton`](https://github.com/digitalwalletcorp/vue-core/blob/main/docs/components/label-button.md) | Renders a button containing an icon and a text label. |
131
+ | [`TabGroup` / `TabContent`](https://github.com/digitalwalletcorp/vue-core/blob/main/docs/components/tab-group.md) | Switches between contents with tab headers. |
103
132
 
104
133
  #### 📜 License
105
134
 
@@ -0,0 +1 @@
1
+ "use strict";const e=require("./accordion-fieldset.vue_vue_type_style_index_0_lang-Cod0KnrE.cjs"),_=require("./hourglass-loading.vue_vue_type_style_index_0_lang-C72UVA9W.cjs"),n=require("./label-button.vue_vue_type_script_setup_true_lang-DqhdCgmJ.cjs"),t=require("./label-icon.vue_vue_type_style_index_0_lang-C2URq6GP.cjs"),o=require("./tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs"),s=require("./tab-group.vue_vue_type_style_index_0_lang-CwVGYZAo.cjs"),u=Object.freeze(Object.defineProperty({__proto__:null,AccordionFieldset:e._sfc_main,HourglassLoading:_._sfc_main,LabelButton:n._sfc_main,LabelIcon:t._sfc_main,TabContent:o._sfc_main,TabGroup:s._sfc_main},Symbol.toStringTag,{value:"Module"}));exports.components=u;
@@ -0,0 +1,18 @@
1
+ import { _ as o } from "./accordion-fieldset.vue_vue_type_style_index_0_lang-Dk5Id0dl.js";
2
+ import { _ } from "./hourglass-loading.vue_vue_type_style_index_0_lang-DkUGIxQ2.js";
3
+ import { _ as a } from "./label-button.vue_vue_type_script_setup_true_lang-CoHrTUSs.js";
4
+ import { _ as r } from "./label-icon.vue_vue_type_style_index_0_lang-bQFkQ_gC.js";
5
+ import { _ as m } from "./tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js";
6
+ import { _ as t } from "./tab-group.vue_vue_type_style_index_0_lang-CjnBkvvZ.js";
7
+ const p = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
8
+ __proto__: null,
9
+ AccordionFieldset: o,
10
+ HourglassLoading: _,
11
+ LabelButton: a,
12
+ LabelIcon: r,
13
+ TabContent: m,
14
+ TabGroup: t
15
+ }, Symbol.toStringTag, { value: "Module" }));
16
+ export {
17
+ p as c
18
+ };
@@ -1,3 +1,6 @@
1
1
  export { default as AccordionFieldset } from './accordion-fieldset';
2
+ export { default as HourglassLoading } from './hourglass-loading';
2
3
  export { default as LabelButton } from './label-button';
3
4
  export { default as LabelIcon } from './label-icon';
5
+ export { default as TabContent } from './tab-content';
6
+ export { default as TabGroup } from './tab-group';
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./hourglass-loading.vue_vue_type_style_index_0_lang-C72UVA9W.cjs");exports.default=e._sfc_main;
@@ -0,0 +1,23 @@
1
+ interface Props {
2
+ /** ローディング表示するか */
3
+ loading: boolean;
4
+ /** 砂が落ち切るまでの秒数。未指定ならSvgHourglassの既定値に任せる */
5
+ duration?: number;
6
+ /** 表示サイズ(高さ)。数値はpxとして扱う */
7
+ size?: number | string;
8
+ }
9
+ declare var __VLS_8: {};
10
+ type __VLS_Slots = {} & {
11
+ default?: (props: typeof __VLS_8) => any;
12
+ };
13
+ declare const __VLS_base: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {
14
+ size: number | string;
15
+ }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
16
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
17
+ declare const _default: typeof __VLS_export;
18
+ export default _default;
19
+ type __VLS_WithSlots<T, S> = T & {
20
+ new (): {
21
+ $slots: S;
22
+ };
23
+ };
@@ -0,0 +1,4 @@
1
+ import { _ as f } from "./hourglass-loading.vue_vue_type_style_index_0_lang-DkUGIxQ2.js";
2
+ export {
3
+ f as default
4
+ };
@@ -0,0 +1 @@
1
+ "use strict";const e=require("vue"),r=require("@digitalwalletcorp/vue-svg-icons"),i=e.defineComponent({__name:"hourglass-loading",props:{loading:{type:Boolean},duration:{},size:{default:"1.2em"}},setup(n){const o=n;return(s,a)=>o.loading?(e.openBlock(),e.createBlock(e.unref(r.SvgHourglass),{key:0,duration:o.duration,size:o.size,class:"dwui-hourglass-loading"},null,8,["duration","size"])):e.renderSlot(s.$slots,"default",{},void 0,void 0,1)}});exports._sfc_main=i;
@@ -0,0 +1,22 @@
1
+ import { defineComponent as r, openBlock as s, createBlock as a, unref as i, renderSlot as t } from "vue";
2
+ import { SvgHourglass as l } from "@digitalwalletcorp/vue-svg-icons";
3
+ const c = /* @__PURE__ */ r({
4
+ __name: "hourglass-loading",
5
+ props: {
6
+ loading: { type: Boolean },
7
+ duration: {},
8
+ size: { default: "1.2em" }
9
+ },
10
+ setup(e) {
11
+ const o = e;
12
+ return (n, d) => o.loading ? (s(), a(i(l), {
13
+ key: 0,
14
+ duration: o.duration,
15
+ size: o.size,
16
+ class: "dwui-hourglass-loading"
17
+ }, null, 8, ["duration", "size"])) : t(n.$slots, "default", {}, void 0, void 0, 1);
18
+ }
19
+ });
20
+ export {
21
+ c as _
22
+ };
package/lib/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./accordion-fieldset.vue_vue_type_style_index_0_lang-Cod0KnrE.cjs"),_=require("./label-button.vue_vue_type_script_setup_true_lang-DqhdCgmJ.cjs"),t=require("./label-icon.vue_vue_type_style_index_0_lang-C2URq6GP.cjs");exports.AccordionFieldset=e._sfc_main;exports.LabelButton=_._sfc_main;exports.LabelIcon=t._sfc_main;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("./accordion-fieldset.vue_vue_type_style_index_0_lang-Cod0KnrE.cjs"),_=require("./hourglass-loading.vue_vue_type_style_index_0_lang-C72UVA9W.cjs"),t=require("./label-button.vue_vue_type_script_setup_true_lang-DqhdCgmJ.cjs"),n=require("./label-icon.vue_vue_type_style_index_0_lang-C2URq6GP.cjs"),u=require("./tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs"),a=require("./tab-group.vue_vue_type_style_index_0_lang-CwVGYZAo.cjs");exports.AccordionFieldset=e._sfc_main;exports.HourglassLoading=_._sfc_main;exports.LabelButton=t._sfc_main;exports.LabelIcon=n._sfc_main;exports.TabContent=u._sfc_main;exports.TabGroup=a._sfc_main;
package/lib/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './components';
2
2
  export type { LabelPreset } from './types/label-preset';
3
+ export type { TabContentStatus } from './internal/tab-group-context';
package/lib/index.js CHANGED
@@ -1,8 +1,14 @@
1
- import { _ as r } from "./accordion-fieldset.vue_vue_type_style_index_0_lang-Dk5Id0dl.js";
2
- import { _ as a } from "./label-button.vue_vue_type_script_setup_true_lang-CoHrTUSs.js";
3
- import { _ as c } from "./label-icon.vue_vue_type_style_index_0_lang-bQFkQ_gC.js";
1
+ import { _ as a } from "./accordion-fieldset.vue_vue_type_style_index_0_lang-Dk5Id0dl.js";
2
+ import { _ as t } from "./hourglass-loading.vue_vue_type_style_index_0_lang-DkUGIxQ2.js";
3
+ import { _ as p } from "./label-button.vue_vue_type_script_setup_true_lang-CoHrTUSs.js";
4
+ import { _ as m } from "./label-icon.vue_vue_type_style_index_0_lang-bQFkQ_gC.js";
5
+ import { _ as x } from "./tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js";
6
+ import { _ as b } from "./tab-group.vue_vue_type_style_index_0_lang-CjnBkvvZ.js";
4
7
  export {
5
- r as AccordionFieldset,
6
- a as LabelButton,
7
- c as LabelIcon
8
+ a as AccordionFieldset,
9
+ t as HourglassLoading,
10
+ p as LabelButton,
11
+ m as LabelIcon,
12
+ x as TabContent,
13
+ b as TabGroup
8
14
  };
@@ -0,0 +1,16 @@
1
+ import { ComputedRef, InjectionKey, Ref, Slot } from 'vue';
2
+ /** タブの見出しの状態 */
3
+ export type TabContentStatus = 'warn' | 'error';
4
+ /**
5
+ * TabGroupからTabContentへ渡す状態
6
+ * TabGroupとTabContentの間だけで使い、利用側には公開しない
7
+ */
8
+ export interface TabGroupContext {
9
+ /** 選択中のタブのlabelId */
10
+ activeLabelId: ComputedRef<string | undefined>;
11
+ /** TabGroupがマウント済みか */
12
+ mounted: Ref<boolean>;
13
+ /** TabGroupの#loadingスロット。未指定ならTabContentは既定の読み込み中表示を出す */
14
+ loadingSlot: () => Slot | undefined;
15
+ }
16
+ export declare const tabGroupContextKey: InjectionKey<TabGroupContext>;
package/lib/nuxt.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("@nuxt/kit"),r=require("./components-CrmajA9z.cjs");var t=typeof document<"u"?document.currentScript:null;const u=e.defineNuxtModule({meta:{name:"@digitalwalletcorp/vue-core",configKey:"vueCore"},setup(){const n=e.createResolver(typeof document>"u"?require("url").pathToFileURL(__filename).href:t&&t.tagName.toUpperCase()==="SCRIPT"&&t.src||new URL("nuxt.cjs",document.baseURI).href);for(const o of Object.keys(r.components))e.addComponent({name:o,export:o,filePath:n.resolve("./index")})}});exports.default=u;
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("@nuxt/kit"),r=require("./components-CvmuAs2C.cjs");var t=typeof document<"u"?document.currentScript:null;const u=e.defineNuxtModule({meta:{name:"@digitalwalletcorp/vue-core",configKey:"vueCore"},setup(){const n=e.createResolver(typeof document>"u"?require("url").pathToFileURL(__filename).href:t&&t.tagName.toUpperCase()==="SCRIPT"&&t.src||new URL("nuxt.cjs",document.baseURI).href);for(const o of Object.keys(r.components))e.addComponent({name:o,export:o,filePath:n.resolve("./index")})}});exports.default=u;
package/lib/nuxt.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineNuxtModule as t, createResolver as r, addComponent as n } from "@nuxt/kit";
2
- import { c as a } from "./components-B3xZ0gFq.js";
2
+ import { c as a } from "./components-p1BluzXb.js";
3
3
  const c = t({
4
4
  meta: {
5
5
  name: "@digitalwalletcorp/vue-core",
package/lib/register.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./components-CrmajA9z.cjs");function r(e){for(const[o,n]of Object.entries(t.components))e.component(o,n)}exports.registerComponents=r;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const t=require("./components-CvmuAs2C.cjs");function r(e){for(const[o,n]of Object.entries(t.components))e.component(o,n)}exports.registerComponents=r;
package/lib/register.js CHANGED
@@ -1,4 +1,4 @@
1
- import { c as t } from "./components-B3xZ0gFq.js";
1
+ import { c as t } from "./components-p1BluzXb.js";
2
2
  function m(o) {
3
3
  for (const [n, e] of Object.entries(t))
4
4
  o.component(n, e);
package/lib/style.css CHANGED
@@ -1 +1 @@
1
- :where(.dwui-label-icon){display:inline-block}:where(.dwui-label-icon>.dwui-label){display:inline-block}:where(.dwui-label-icon>.dwui-label:not(:first-child)){margin-left:var(--dwui-label-icon-gap, .25em)}:where(.dwui-accordion-fieldset){position:relative}:where(.dwui-accordion-toggle){display:inline-flex;align-items:center;gap:4px;margin-left:4px;padding:0;border:none;background:none;color:inherit;cursor:pointer}:where(.dwui-accordion-toggle:disabled){opacity:.6;cursor:not-allowed}:where(.dwui-accordion-contents){position:relative;width:fit-content;padding:2px;overflow:hidden;opacity:1}:where(.dwui-accordion-contents.dwui-expanded){transition:all .3s linear}:where(.dwui-accordion-contents.dwui-shrunk){height:auto;max-height:70px;opacity:.75;transition:all .3s linear}
1
+ :where(.dwui-label-icon){display:inline-block}:where(.dwui-label-icon>.dwui-label){display:inline-block}:where(.dwui-label-icon>.dwui-label:not(:first-child)){margin-left:var(--dwui-label-icon-gap, .25em)}:where(.dwui-accordion-fieldset){position:relative}:where(.dwui-accordion-toggle){display:inline-flex;align-items:center;gap:4px;margin-left:4px;padding:0;border:none;background:none;color:inherit;cursor:pointer}:where(.dwui-accordion-toggle:disabled){opacity:.6;cursor:not-allowed}:where(.dwui-accordion-contents){position:relative;width:fit-content;padding:2px;overflow:hidden;opacity:1}:where(.dwui-accordion-contents.dwui-expanded){transition:all .3s linear}:where(.dwui-accordion-contents.dwui-shrunk){height:auto;max-height:70px;opacity:.75;transition:all .3s linear}:where(.dwui-hourglass-loading){animation:dwui-hourglass-loading-spin 1.2s linear infinite}@keyframes dwui-hourglass-loading-spin{to{transform:rotate(360deg)}}:where(.dwui-tab-content[aria-checked=false]){display:none}:where(.dwui-tab-content[aria-checked=true]){display:block;position:relative;padding:4px 8px;overflow:hidden;animation:dwui-tab-slide-in .35s cubic-bezier(.22,1,.36,1)}:where(.dwui-tab-loading){position:absolute;inset:0;z-index:1;display:flex;align-items:center;justify-content:center;background:var(--dwui-loading-overlay, rgba(0, 0, 0, .25))}:where(.dwui-tab-loading-spinner){width:var(--dwui-loading-spinner-size, 40px);height:var(--dwui-loading-spinner-size, 40px);border:4px solid var(--dwui-loading-spinner-track, rgba(255, 255, 255, .4));border-top-color:var(--dwui-loading-spinner-color, #ffffff);border-radius:50%;animation:dwui-tab-loading-spin .8s linear infinite}@keyframes dwui-tab-loading-spin{to{transform:rotate(360deg)}}@keyframes dwui-tab-slide-in{0%{opacity:0;transform:translate(-128px)}to{opacity:1;transform:translate(0)}}:where(.dwui-tab-group){margin-bottom:8px}:where(.dwui-tab-headers){display:flex;flex-direction:row;flex-wrap:wrap;margin-top:5px;padding-bottom:4px}:where(.dwui-tab-radio){display:none}:where(.dwui-tab-item){display:block;min-width:100px;padding:0 4px;border:1px solid var(--dwui-tab-border, #565656);border-bottom:1px solid var(--dwui-tab-header-accent, #5ab4bd);background:var(--dwui-tab-header-bg, linear-gradient(#1313c7, navy));line-height:2em;text-align:center;white-space:nowrap;color:var(--dwui-tab-header-text, azure);text-overflow:ellipsis;cursor:pointer;transition:all .2s ease}:where(.dwui-tab-radio:checked+.dwui-tab-item){background:var(--dwui-tab-header-selected-bg, linear-gradient(#1313c7, rgb(149, 149, 234), #1313c7));color:var(--dwui-tab-header-selected-text, azure);cursor:default}:where(.dwui-tab-radio:disabled+.dwui-tab-item){background:var(--dwui-tab-header-disabled-bg, gray);cursor:not-allowed}:where(.dwui-tab-item.dwui-tab-warn){background:var(--dwui-tab-header-warn-bg, #efef13);color:var(--dwui-tab-header-warn-text, midnightblue)}:where(.dwui-tab-radio:checked+.dwui-tab-item.dwui-tab-warn){background:var(--dwui-tab-header-warn-selected-bg, linear-gradient(#fafa06, rgb(232, 238, 206), #fafa06))}:where(.dwui-tab-item.dwui-tab-error){background:var(--dwui-tab-header-error-bg, red);color:var(--dwui-tab-header-error-text, azure)}:where(.dwui-tab-radio:checked+.dwui-tab-item.dwui-tab-error){background:var(--dwui-tab-header-error-selected-bg, linear-gradient(#ff0000, rgb(240, 136, 136), #ff0000))}:where(.dwui-tab-radio:not(:checked)+.dwui-tab-item:hover){box-shadow:inset 0 0 0 999px var(--dwui-tab-header-hover-overlay, rgba(255, 255, 255, .35))}:where(.dwui-tab-items){height:100%;border:1px solid var(--dwui-tab-border, #565656);overflow:hidden}
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs");exports.default=e._sfc_main;
@@ -0,0 +1,39 @@
1
+ import { ClassValue, StyleValue } from 'vue';
2
+ import { TabContentStatus } from './internal/tab-group-context';
3
+ interface Props {
4
+ /** タブを識別するID。TabGroupのselectedTabLabelIdに渡す値 */
5
+ labelId: string;
6
+ /** タブの見出しの文字 */
7
+ description: string;
8
+ /** trueの場合、タブを選べなくし、中身も描画しない */
9
+ disabled?: boolean;
10
+ /** タブの見出しの状態。見出しの色が変わる */
11
+ status?: TabContentStatus;
12
+ /** タブの見出しに付けるclass */
13
+ headerClass?: ClassValue;
14
+ /** タブの見出しに付けるstyle */
15
+ headerStyle?: StyleValue;
16
+ }
17
+ interface Slots {
18
+ /** タブの見出しの文字の前に表示するアイコン */
19
+ icon?(): unknown;
20
+ /**
21
+ * タブの中身
22
+ * activated: タブが選ばれて表示されている間はtrue。中身のコンポーネントにpropsで渡し、選ばれたときの処理の契機にする
23
+ * setLoading: trueを渡すと読み込み中の表示にする。表示はTabGroupの#loadingスロット、未指定なら既定の表示
24
+ */
25
+ default?(props: {
26
+ activated: boolean;
27
+ setLoading: (loading: boolean) => void;
28
+ }): unknown;
29
+ }
30
+ type __VLS_Slots = Slots;
31
+ declare const __VLS_base: import('vue').DefineComponent<Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
32
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
33
+ declare const _default: typeof __VLS_export;
34
+ export default _default;
35
+ type __VLS_WithSlots<T, S> = T & {
36
+ new (): {
37
+ $slots: S;
38
+ };
39
+ };
@@ -0,0 +1,4 @@
1
+ import { _ as f } from "./tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js";
2
+ export {
3
+ f as default
4
+ };
@@ -0,0 +1 @@
1
+ "use strict";const e=require("vue"),s=Symbol("dwui-tab-group"),u=["aria-label","aria-checked","aria-description","aria-busy","data-test-tab-content"],p={key:0,class:"dwui-tab-loading"},b={key:1,class:"dwui-tab-loading-spinner"},m=e.defineComponent({__name:"tab-content",props:{labelId:{},description:{},disabled:{type:Boolean},status:{},headerClass:{type:[Boolean,null,String,Object,Array]},headerStyle:{type:[Boolean,null,String,Object,Array]}},setup(i){const t=i,a=e.inject(s,void 0),o=e.ref(!1),l=()=>a?.loadingSlot(),c=e.computed(()=>a?.activeLabelId.value===t.labelId),d=e.computed(()=>!!a?.mounted.value&&c.value),r=n=>{o.value=n};return(n,v)=>t.disabled?e.createCommentVNode("",!0):(e.openBlock(),e.createElementBlock("div",{key:0,class:"dwui-tab-content","aria-label":t.labelId,"aria-checked":c.value?"true":"false","aria-description":t.description,"aria-busy":o.value?"true":void 0,"data-test-tab-content":t.labelId},[e.renderSlot(n.$slots,"default",{activated:d.value,setLoading:r}),o.value?(e.openBlock(),e.createElementBlock("div",p,[l()!=null?(e.openBlock(),e.createBlock(e.resolveDynamicComponent(l()),{key:0})):(e.openBlock(),e.createElementBlock("span",b))])):e.createCommentVNode("",!0)],8,u))}});exports._sfc_main=m;exports.tabGroupContextKey=s;
@@ -0,0 +1,44 @@
1
+ import { defineComponent as p, inject as v, ref as y, computed as i, openBlock as t, createElementBlock as l, renderSlot as m, createBlock as _, resolveDynamicComponent as g, createCommentVNode as d } from "vue";
2
+ const k = /* @__PURE__ */ Symbol("dwui-tab-group"), h = ["aria-label", "aria-checked", "aria-description", "aria-busy", "data-test-tab-content"], f = {
3
+ key: 0,
4
+ class: "dwui-tab-loading"
5
+ }, S = {
6
+ key: 1,
7
+ class: "dwui-tab-loading-spinner"
8
+ }, I = /* @__PURE__ */ p({
9
+ __name: "tab-content",
10
+ props: {
11
+ labelId: {},
12
+ description: {},
13
+ disabled: { type: Boolean },
14
+ status: {},
15
+ headerClass: { type: [Boolean, null, String, Object, Array] },
16
+ headerStyle: { type: [Boolean, null, String, Object, Array] }
17
+ },
18
+ setup(r) {
19
+ const e = r, a = v(k, void 0), o = y(!1), s = () => a?.loadingSlot(), c = i(() => a?.activeLabelId.value === e.labelId), u = i(() => !!a?.mounted.value && c.value), b = (n) => {
20
+ o.value = n;
21
+ };
22
+ return (n, B) => e.disabled ? d("", !0) : (t(), l("div", {
23
+ key: 0,
24
+ class: "dwui-tab-content",
25
+ "aria-label": e.labelId,
26
+ "aria-checked": c.value ? "true" : "false",
27
+ "aria-description": e.description,
28
+ "aria-busy": o.value ? "true" : void 0,
29
+ "data-test-tab-content": e.labelId
30
+ }, [
31
+ m(n.$slots, "default", {
32
+ activated: u.value,
33
+ setLoading: b
34
+ }),
35
+ o.value ? (t(), l("div", f, [
36
+ s() != null ? (t(), _(g(s()), { key: 0 })) : (t(), l("span", S))
37
+ ])) : d("", !0)
38
+ ], 8, h));
39
+ }
40
+ });
41
+ export {
42
+ I as _,
43
+ k as t
44
+ };
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const e=require("./tab-group.vue_vue_type_style_index_0_lang-CwVGYZAo.cjs");exports.default=e._sfc_main;
@@ -0,0 +1,44 @@
1
+ import { ClassValue, StyleValue, VNode } from 'vue';
2
+ interface Props {
3
+ /** ラジオボタンのname。未指定なら自動で採番する */
4
+ tabGroupName?: string;
5
+ /** selectedTabLabelIdが未指定のとき、trueならdisabledでない最初のタブを選ぶ。falseなら先頭のタブ(disabledなら中身は空になる) */
6
+ selectFirstEnabled?: boolean;
7
+ /** タブの見出しを並べる部分に付けるclass */
8
+ headerClass?: ClassValue;
9
+ /** タブの見出しを並べる部分に付けるstyle */
10
+ headerStyle?: StyleValue;
11
+ /** タブの見出しを移動する先(Teleportのto) */
12
+ teleport?: string;
13
+ }
14
+ type __VLS_Props = Props;
15
+ interface Slots {
16
+ default?(): VNode[];
17
+ /** 読み込み中のTabContentに重ねて表示する内容。未指定なら既定の読み込み中表示 */
18
+ loading?(): VNode[];
19
+ }
20
+ type __VLS_Slots = Slots;
21
+ type __VLS_ModelProps = {
22
+ /**
23
+ * 選択するタブのlabelId。未指定なら先頭のタブ
24
+ * 親がいつ代入しても選び直せるよう、v-model:selected-tab-label-idで同期する前提とする
25
+ */
26
+ 'selectedTabLabelId'?: string;
27
+ };
28
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
29
+ declare const __VLS_base: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
30
+ "update:selectedTabLabelId": (value: string | undefined) => any;
31
+ } & {
32
+ "emit:changeSelectedTab": (labelId: string) => any;
33
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
34
+ "onEmit:changeSelectedTab"?: ((labelId: string) => any) | undefined;
35
+ "onUpdate:selectedTabLabelId"?: ((value: string | undefined) => any) | undefined;
36
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
37
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
38
+ declare const _default: typeof __VLS_export;
39
+ export default _default;
40
+ type __VLS_WithSlots<T, S> = T & {
41
+ new (): {
42
+ $slots: S;
43
+ };
44
+ };
@@ -0,0 +1,4 @@
1
+ import { _ as f } from "./tab-group.vue_vue_type_style_index_0_lang-CjnBkvvZ.js";
2
+ export {
3
+ f as default
4
+ };
@@ -0,0 +1,103 @@
1
+ import { defineComponent as L, useModel as B, useSlots as N, useId as x, computed as y, ref as A, provide as E, onMounted as M, openBlock as s, createElementBlock as i, createBlock as _, Teleport as F, createElementVNode as o, normalizeStyle as v, normalizeClass as g, Fragment as S, renderList as G, unref as c, createVNode as V, createSlots as j, withCtx as I, createTextVNode as z, toDisplayString as D, resolveDynamicComponent as O, renderSlot as H, mergeModels as C } from "vue";
2
+ import { _ as K } from "./label-icon.vue_vue_type_style_index_0_lang-bQFkQ_gC.js";
3
+ import { t as P, _ as q } from "./tab-content.vue_vue_type_style_index_0_lang-CcQLHmIF.js";
4
+ const J = { class: "dwui-tab-group" }, Q = ["id", "name", "disabled", "checked", "onChange"], R = ["for", "data-test-tab-content-control"], U = { class: "dwui-tab-items" }, Z = /* @__PURE__ */ L({
5
+ __name: "tab-group",
6
+ props: /* @__PURE__ */ C({
7
+ tabGroupName: {},
8
+ selectFirstEnabled: { type: Boolean },
9
+ headerClass: { type: [Boolean, null, String, Object, Array] },
10
+ headerStyle: { type: [Boolean, null, String, Object, Array] },
11
+ teleport: {}
12
+ }, {
13
+ selectedTabLabelId: {},
14
+ selectedTabLabelIdModifiers: {}
15
+ }),
16
+ emits: /* @__PURE__ */ C(["emit:changeSelectedTab"], ["update:selectedTabLabelId"]),
17
+ setup(u, { emit: T }) {
18
+ const l = u, $ = T, d = B(u, "selectedTabLabelId"), b = N(), n = l.tabGroupName ?? x(), k = (a) => {
19
+ if (a.type !== q)
20
+ return;
21
+ const t = a.props ?? {}, e = t.disabled;
22
+ return {
23
+ labelId: String(t["label-id"] ?? t.labelId),
24
+ description: t.description,
25
+ // 属性だけを書いた場合(<TabContent disabled>)は空文字になる
26
+ disabled: e === "" || !!e,
27
+ status: t.status,
28
+ headerClass: t["header-class"] ?? t.headerClass,
29
+ headerStyle: t["header-style"] ?? t.headerStyle,
30
+ // TabContentの#iconスロットは、TabContentではなくここで見出しに描画する
31
+ icon: a.children?.icon
32
+ };
33
+ }, r = y(() => {
34
+ const a = [];
35
+ for (const t of b.default?.() ?? []) {
36
+ const e = Array.isArray(t.children) ? t.children : [t];
37
+ for (const h of e) {
38
+ const f = k(h);
39
+ f && a.push(f);
40
+ }
41
+ }
42
+ return a;
43
+ }), p = y(() => d.value != null ? d.value : (l.selectFirstEnabled ? r.value.find((t) => !t.disabled) : r.value[0])?.labelId), m = A(!1);
44
+ E(P, { activeLabelId: p, mounted: m, loadingSlot: () => b.loading }), M(() => {
45
+ m.value = !0;
46
+ });
47
+ const w = (a) => {
48
+ d.value = a, $("emit:changeSelectedTab", a);
49
+ };
50
+ return (a, t) => (s(), i("div", J, [
51
+ (s(), _(F, {
52
+ to: l.teleport ?? "body",
53
+ disabled: l.teleport == null
54
+ }, [
55
+ o("div", {
56
+ class: g(["dwui-tab-headers", l.headerClass]),
57
+ style: v(l.headerStyle)
58
+ }, [
59
+ (s(!0), i(S, null, G(r.value, (e) => (s(), i(S, {
60
+ key: e.labelId
61
+ }, [
62
+ o("input", {
63
+ id: `${c(n)}-${e.labelId}`,
64
+ type: "radio",
65
+ class: "dwui-tab-radio",
66
+ name: c(n),
67
+ disabled: e.disabled,
68
+ checked: e.labelId === p.value,
69
+ onChange: (h) => w(e.labelId)
70
+ }, null, 40, Q),
71
+ o("label", {
72
+ for: `${c(n)}-${e.labelId}`,
73
+ class: g(["dwui-tab-item", [e.status != null ? `dwui-tab-${e.status}` : void 0, e.headerClass]]),
74
+ style: v(e.headerStyle),
75
+ "data-test-tab-content-control": e.labelId
76
+ }, [
77
+ V(K, null, j({
78
+ default: I(() => [
79
+ z(" " + D(e.description), 1)
80
+ ]),
81
+ _: 2
82
+ }, [
83
+ e.icon != null ? {
84
+ name: "icon",
85
+ fn: I(() => [
86
+ (s(), _(O(e.icon)))
87
+ ]),
88
+ key: "0"
89
+ } : void 0
90
+ ]), 1024)
91
+ ], 14, R)
92
+ ], 64))), 128))
93
+ ], 6)
94
+ ], 8, ["to", "disabled"])),
95
+ o("div", U, [
96
+ H(a.$slots, "default")
97
+ ])
98
+ ]));
99
+ }
100
+ });
101
+ export {
102
+ Z as _
103
+ };
@@ -0,0 +1 @@
1
+ "use strict";const e=require("vue"),v=require("./label-icon.vue_vue_type_style_index_0_lang-C2URq6GP.cjs"),m=require("./tab-content.vue_vue_type_style_index_0_lang-BeCi7Xkp.cjs"),g={class:"dwui-tab-group"},S=["id","name","disabled","checked","onChange"],C=["for","data-test-tab-content-control"],I={class:"dwui-tab-items"},k=e.defineComponent({__name:"tab-group",props:e.mergeModels({tabGroupName:{},selectFirstEnabled:{type:Boolean},headerClass:{type:[Boolean,null,String,Object,Array]},headerStyle:{type:[Boolean,null,String,Object,Array]},teleport:{}},{selectedTabLabelId:{},selectedTabLabelIdModifiers:{}}),emits:e.mergeModels(["emit:changeSelectedTab"],["update:selectedTabLabelId"]),setup(r,{emit:h}){const n=r,_=h,o=e.useModel(r,"selectedTabLabelId"),c=e.useSlots(),s=n.tabGroupName??e.useId(),y=a=>{if(a.type!==m._sfc_main)return;const l=a.props??{},t=l.disabled;return{labelId:String(l["label-id"]??l.labelId),description:l.description,disabled:t===""||!!t,status:l.status,headerClass:l["header-class"]??l.headerClass,headerStyle:l["header-style"]??l.headerStyle,icon:a.children?.icon}},d=e.computed(()=>{const a=[];for(const l of c.default?.()??[]){const t=Array.isArray(l.children)?l.children:[l];for(const b of t){const p=y(b);p&&a.push(p)}}return a}),i=e.computed(()=>o.value!=null?o.value:(n.selectFirstEnabled?d.value.find(l=>!l.disabled):d.value[0])?.labelId),u=e.ref(!1);e.provide(m.tabGroupContextKey,{activeLabelId:i,mounted:u,loadingSlot:()=>c.loading}),e.onMounted(()=>{u.value=!0});const f=a=>{o.value=a,_("emit:changeSelectedTab",a)};return(a,l)=>(e.openBlock(),e.createElementBlock("div",g,[(e.openBlock(),e.createBlock(e.Teleport,{to:n.teleport??"body",disabled:n.teleport==null},[e.createElementVNode("div",{class:e.normalizeClass(["dwui-tab-headers",n.headerClass]),style:e.normalizeStyle(n.headerStyle)},[(e.openBlock(!0),e.createElementBlock(e.Fragment,null,e.renderList(d.value,t=>(e.openBlock(),e.createElementBlock(e.Fragment,{key:t.labelId},[e.createElementVNode("input",{id:`${e.unref(s)}-${t.labelId}`,type:"radio",class:"dwui-tab-radio",name:e.unref(s),disabled:t.disabled,checked:t.labelId===i.value,onChange:b=>f(t.labelId)},null,40,S),e.createElementVNode("label",{for:`${e.unref(s)}-${t.labelId}`,class:e.normalizeClass(["dwui-tab-item",[t.status!=null?`dwui-tab-${t.status}`:void 0,t.headerClass]]),style:e.normalizeStyle(t.headerStyle),"data-test-tab-content-control":t.labelId},[e.createVNode(v._sfc_main,null,e.createSlots({default:e.withCtx(()=>[e.createTextVNode(" "+e.toDisplayString(t.description),1)]),_:2},[t.icon!=null?{name:"icon",fn:e.withCtx(()=>[(e.openBlock(),e.createBlock(e.resolveDynamicComponent(t.icon)))]),key:"0"}:void 0]),1024)],14,C)],64))),128))],6)],8,["to","disabled"])),e.createElementVNode("div",I,[e.renderSlot(a.$slots,"default")])]))}});exports._sfc_main=k;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digitalwalletcorp/vue-core",
3
- "version": "0.3.0",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Basic UI components for Vue 3",
6
6
  "main": "lib/index.cjs",
package/src/components.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  // 公開するコンポーネントをここに列挙する。index/register/nuxtはここから読む
2
2
  export { default as AccordionFieldset } from '@/accordion-fieldset.vue';
3
+ export { default as HourglassLoading } from '@/hourglass-loading.vue';
3
4
  export { default as LabelButton } from '@/label-button.vue';
4
5
  export { default as LabelIcon } from '@/label-icon.vue';
6
+ export { default as TabContent } from '@/tab-content.vue';
7
+ export { default as TabGroup } from '@/tab-group.vue';
@@ -0,0 +1,42 @@
1
+ <!--
2
+ 砂時計のローディング。
3
+ loading=true の間は砂時計アイコン(SvgHourglass)を表示し、false になったら slot の内容(空文字でもそのまま)を表示する。
4
+ CSSの :empty に依存しないため「取得成功したが値が空」のケースでも loading が残らない。
5
+ -->
6
+ <template>
7
+ <SvgHourglass
8
+ v-if="props.loading"
9
+ :duration="props.duration"
10
+ :size="props.size"
11
+ class="dwui-hourglass-loading"
12
+ />
13
+ <slot v-else />
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { SvgHourglass } from '@digitalwalletcorp/vue-svg-icons';
18
+
19
+ interface Props {
20
+ /** ローディング表示するか */
21
+ loading: boolean;
22
+ /** 砂が落ち切るまでの秒数。未指定ならSvgHourglassの既定値に任せる */
23
+ duration?: number;
24
+ /** 表示サイズ(高さ)。数値はpxとして扱う */
25
+ size?: number | string;
26
+ }
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ size: '1.2em'
29
+ });
30
+ </script>
31
+
32
+ <style>
33
+ :where(.dwui-hourglass-loading) {
34
+ animation: dwui-hourglass-loading-spin 1.2s linear infinite;
35
+ }
36
+
37
+ @keyframes dwui-hourglass-loading-spin {
38
+ to {
39
+ transform: rotate(360deg);
40
+ }
41
+ }
42
+ </style>
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from '@/components';
2
2
  export type { LabelPreset } from '@/types/label-preset';
3
+ export type { TabContentStatus } from '@/internal/tab-group-context';
@@ -0,0 +1,19 @@
1
+ import type { ComputedRef, InjectionKey, Ref, Slot } from 'vue';
2
+
3
+ /** タブの見出しの状態 */
4
+ export type TabContentStatus = 'warn' | 'error';
5
+
6
+ /**
7
+ * TabGroupからTabContentへ渡す状態
8
+ * TabGroupとTabContentの間だけで使い、利用側には公開しない
9
+ */
10
+ export interface TabGroupContext {
11
+ /** 選択中のタブのlabelId */
12
+ activeLabelId: ComputedRef<string | undefined>;
13
+ /** TabGroupがマウント済みか */
14
+ mounted: Ref<boolean>;
15
+ /** TabGroupの#loadingスロット。未指定ならTabContentは既定の読み込み中表示を出す */
16
+ loadingSlot: () => Slot | undefined;
17
+ }
18
+
19
+ export const tabGroupContextKey: InjectionKey<TabGroupContext> = Symbol('dwui-tab-group');
@@ -0,0 +1,147 @@
1
+ <!--
2
+ TabGroupの中に置くタブ1つ分の中身
3
+ -->
4
+ <template>
5
+ <div
6
+ v-if="!props.disabled"
7
+ class="dwui-tab-content"
8
+ :aria-label="props.labelId"
9
+ :aria-checked="selected ? 'true' : 'false'"
10
+ :aria-description="props.description"
11
+ :aria-busy="loading ? 'true' : undefined"
12
+ :data-test-tab-content="props.labelId"
13
+ >
14
+ <slot
15
+ :activated="activated"
16
+ :set-loading="setLoading"
17
+ />
18
+ <!-- 読み込み中は中身に覆いを重ね、操作できないようにする -->
19
+ <div
20
+ v-if="loading"
21
+ class="dwui-tab-loading"
22
+ >
23
+ <component
24
+ :is="loadingSlot()"
25
+ v-if="loadingSlot() != null"
26
+ />
27
+ <span
28
+ v-else
29
+ class="dwui-tab-loading-spinner"
30
+ />
31
+ </div>
32
+ </div>
33
+ </template>
34
+
35
+ <script setup lang="ts">
36
+ import { computed, inject, ref } from 'vue';
37
+ import type { ClassValue, Slot, StyleValue } from 'vue';
38
+ import { tabGroupContextKey } from '@/internal/tab-group-context';
39
+ import type { TabContentStatus } from '@/internal/tab-group-context';
40
+
41
+ // labelId・description・header*・status・#iconは、TabGroupが見出しを描画するために読む
42
+ interface Props {
43
+ /** タブを識別するID。TabGroupのselectedTabLabelIdに渡す値 */
44
+ labelId: string;
45
+ /** タブの見出しの文字 */
46
+ description: string;
47
+ /** trueの場合、タブを選べなくし、中身も描画しない */
48
+ disabled?: boolean;
49
+ /** タブの見出しの状態。見出しの色が変わる */
50
+ status?: TabContentStatus;
51
+ /** タブの見出しに付けるclass */
52
+ headerClass?: ClassValue;
53
+ /** タブの見出しに付けるstyle */
54
+ headerStyle?: StyleValue;
55
+ }
56
+ const props = defineProps<Props>();
57
+
58
+ interface Slots {
59
+ /** タブの見出しの文字の前に表示するアイコン */
60
+ icon?(): unknown;
61
+ /**
62
+ * タブの中身
63
+ * activated: タブが選ばれて表示されている間はtrue。中身のコンポーネントにpropsで渡し、選ばれたときの処理の契機にする
64
+ * setLoading: trueを渡すと読み込み中の表示にする。表示はTabGroupの#loadingスロット、未指定なら既定の表示
65
+ */
66
+ default?(props: { activated: boolean; setLoading: (loading: boolean) => void }): unknown;
67
+ }
68
+ defineSlots<Slots>();
69
+
70
+ const context = inject(tabGroupContextKey, undefined);
71
+
72
+ const loading = ref(false);
73
+
74
+ /**
75
+ * TabGroupの#loadingスロット。見出しの#iconと同じく、スロット関数をそのままコンポーネントとして描画する
76
+ * スロットはリアクティブではないため、computedでキャッシュせず描画のたびに取り出す
77
+ *
78
+ * @returns {Slot | undefined}
79
+ */
80
+ const loadingSlot = (): Slot | undefined => context?.loadingSlot();
81
+
82
+ // 表示の切り替えは初期描画から効かせるため、マウントを待たない
83
+ const selected = computed((): boolean => context?.activeLabelId.value === props.labelId);
84
+
85
+ const activated = computed((): boolean => !!context?.mounted.value && selected.value);
86
+
87
+ /**
88
+ * 読み込み中の表示を切り替える
89
+ *
90
+ * @param {boolean} value
91
+ */
92
+ const setLoading = (value: boolean) => {
93
+ loading.value = value;
94
+ };
95
+ </script>
96
+
97
+ <style>
98
+ :where(.dwui-tab-content[aria-checked='false']) {
99
+ display: none;
100
+ }
101
+
102
+ :where(.dwui-tab-content[aria-checked='true']) {
103
+ display: block;
104
+ /* 読み込み中の覆いを中身の範囲に重ねる基準にする */
105
+ position: relative;
106
+ padding: 4px 8px;
107
+ overflow: hidden;
108
+ /* 全幅を横断すると速度が出てカクつくため、固定の移動量とease-outとフェードでスライドインする */
109
+ animation: dwui-tab-slide-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
110
+ }
111
+
112
+ :where(.dwui-tab-loading) {
113
+ position: absolute;
114
+ inset: 0;
115
+ z-index: 1;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ background: var(--dwui-loading-overlay, rgba(0, 0, 0, 0.25));
120
+ }
121
+
122
+ :where(.dwui-tab-loading-spinner) {
123
+ width: var(--dwui-loading-spinner-size, 40px);
124
+ height: var(--dwui-loading-spinner-size, 40px);
125
+ border: 4px solid var(--dwui-loading-spinner-track, rgba(255, 255, 255, 0.4));
126
+ border-top-color: var(--dwui-loading-spinner-color, #ffffff);
127
+ border-radius: 50%;
128
+ animation: dwui-tab-loading-spin 0.8s linear infinite;
129
+ }
130
+
131
+ @keyframes dwui-tab-loading-spin {
132
+ to {
133
+ transform: rotate(360deg);
134
+ }
135
+ }
136
+
137
+ @keyframes dwui-tab-slide-in {
138
+ 0% {
139
+ opacity: 0;
140
+ transform: translateX(-128px);
141
+ }
142
+ 100% {
143
+ opacity: 1;
144
+ transform: translateX(0);
145
+ }
146
+ }
147
+ </style>
@@ -0,0 +1,246 @@
1
+ <!--
2
+ TabContentを並べてタブで切り替える
3
+ -->
4
+ <template>
5
+ <div class="dwui-tab-group">
6
+ <!-- SSRではtoが空だとdisabledでも何も出力されないため、移動しない場合も有効なセレクタを渡す -->
7
+ <Teleport
8
+ :to="props.teleport ?? 'body'"
9
+ :disabled="props.teleport == null"
10
+ >
11
+ <div
12
+ class="dwui-tab-headers"
13
+ :class="props.headerClass"
14
+ :style="props.headerStyle"
15
+ >
16
+ <template
17
+ v-for="tab in tabs"
18
+ :key="tab.labelId"
19
+ >
20
+ <input
21
+ :id="`${groupName}-${tab.labelId}`"
22
+ type="radio"
23
+ class="dwui-tab-radio"
24
+ :name="groupName"
25
+ :disabled="tab.disabled"
26
+ :checked="tab.labelId === activeLabelId"
27
+ @change="onChangeTab(tab.labelId)"
28
+ >
29
+ <label
30
+ :for="`${groupName}-${tab.labelId}`"
31
+ class="dwui-tab-item"
32
+ :class="[tab.status != null ? `dwui-tab-${tab.status}` : undefined, tab.headerClass]"
33
+ :style="tab.headerStyle"
34
+ :data-test-tab-content-control="tab.labelId"
35
+ >
36
+ <LabelIcon>
37
+ <template
38
+ v-if="tab.icon != null"
39
+ #icon
40
+ >
41
+ <component :is="tab.icon" />
42
+ </template>
43
+ {{ tab.description }}
44
+ </LabelIcon>
45
+ </label>
46
+ </template>
47
+ </div>
48
+ </Teleport>
49
+ <div class="dwui-tab-items">
50
+ <slot />
51
+ </div>
52
+ </div>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { computed, onMounted, provide, ref, useId } from 'vue';
57
+ import type { ClassValue, Slot, StyleValue, VNode } from 'vue';
58
+ import LabelIcon from '@/label-icon.vue';
59
+ import TabContent from '@/tab-content.vue';
60
+ import { tabGroupContextKey } from '@/internal/tab-group-context';
61
+ import type { TabContentStatus } from '@/internal/tab-group-context';
62
+
63
+ interface Props {
64
+ /** ラジオボタンのname。未指定なら自動で採番する */
65
+ tabGroupName?: string;
66
+ /** selectedTabLabelIdが未指定のとき、trueならdisabledでない最初のタブを選ぶ。falseなら先頭のタブ(disabledなら中身は空になる) */
67
+ selectFirstEnabled?: boolean;
68
+ /** タブの見出しを並べる部分に付けるclass */
69
+ headerClass?: ClassValue;
70
+ /** タブの見出しを並べる部分に付けるstyle */
71
+ headerStyle?: StyleValue;
72
+ /** タブの見出しを移動する先(Teleportのto) */
73
+ teleport?: string;
74
+ }
75
+ const props = defineProps<Props>();
76
+
77
+ interface Emits {
78
+ (e: 'emit:changeSelectedTab', labelId: string): void;
79
+ }
80
+ const emit = defineEmits<Emits>();
81
+
82
+ /**
83
+ * 選択するタブのlabelId。未指定なら先頭のタブ
84
+ * 親がいつ代入しても選び直せるよう、v-model:selected-tab-label-idで同期する前提とする
85
+ */
86
+ const selectedLabelId = defineModel<string>('selectedTabLabelId');
87
+
88
+ interface Slots {
89
+ default?(): VNode[];
90
+ /** 読み込み中のTabContentに重ねて表示する内容。未指定なら既定の読み込み中表示 */
91
+ loading?(): VNode[];
92
+ }
93
+ const slots = defineSlots<Slots>();
94
+
95
+ /** タブの見出しの描画に使う、TabContentの指定 */
96
+ interface TabHeader {
97
+ labelId: string;
98
+ description: string;
99
+ disabled: boolean;
100
+ status?: TabContentStatus;
101
+ headerClass?: ClassValue;
102
+ headerStyle?: StyleValue;
103
+ icon?: Slot;
104
+ }
105
+
106
+ const groupName = props.tabGroupName ?? useId();
107
+
108
+ /**
109
+ * TabContentのVNodeから見出しの指定を取り出す
110
+ * propsのキーはテンプレートの書き方(label-id / labelId)のまま入っているため、両方を見る
111
+ *
112
+ * @param {VNode} vnode
113
+ * @returns {TabHeader | undefined}
114
+ */
115
+ const toTabHeader = (vnode: VNode): TabHeader | undefined => {
116
+ // v-ifで消えたときのコメントなど、TabContent以外のVNodeは除外する
117
+ if (vnode.type !== TabContent) {
118
+ return undefined;
119
+ }
120
+ const vnodeProps = vnode.props ?? {};
121
+ const disabled = vnodeProps.disabled;
122
+ return {
123
+ labelId: String(vnodeProps['label-id'] ?? vnodeProps.labelId),
124
+ description: vnodeProps.description,
125
+ // 属性だけを書いた場合(<TabContent disabled>)は空文字になる
126
+ disabled: disabled === '' || !!disabled,
127
+ status: vnodeProps.status,
128
+ headerClass: vnodeProps['header-class'] ?? vnodeProps.headerClass,
129
+ headerStyle: vnodeProps['header-style'] ?? vnodeProps.headerStyle,
130
+ // TabContentの#iconスロットは、TabContentではなくここで見出しに描画する
131
+ icon: (vnode.children as Record<string, Slot | undefined> | null)?.icon
132
+ };
133
+ };
134
+
135
+ const tabs = computed((): TabHeader[] => {
136
+ const headers: TabHeader[] = [];
137
+ for (const vnode of slots.default?.() ?? []) {
138
+ // v-forで並べたTabContentは、Fragmentのchildrenに入る
139
+ const candidates = Array.isArray(vnode.children) ? vnode.children as VNode[] : [vnode];
140
+ for (const candidate of candidates) {
141
+ const header = toTabHeader(candidate);
142
+ if (header) {
143
+ headers.push(header);
144
+ }
145
+ }
146
+ }
147
+ return headers;
148
+ });
149
+
150
+ // 選ばれるまでは先頭のタブを選択中とする。スロットは描画中に読む必要があるため、setupの時点では先頭を確定しない
151
+ const activeLabelId = computed((): string | undefined => {
152
+ if (selectedLabelId.value != null) {
153
+ return selectedLabelId.value;
154
+ }
155
+ const first = props.selectFirstEnabled ? tabs.value.find(tab => !tab.disabled) : tabs.value[0];
156
+ return first?.labelId;
157
+ });
158
+ // タブの中身はactivatedを契機にAPIを呼ぶため、親の画面のonMountedで検索条件が整う前に通知しないよう、マウント後に通知を始める
159
+ const mounted = ref(false);
160
+ provide(tabGroupContextKey, { activeLabelId, mounted, loadingSlot: () => slots.loading });
161
+
162
+ onMounted(() => {
163
+ mounted.value = true;
164
+ });
165
+
166
+ const onChangeTab = (labelId: string) => {
167
+ selectedLabelId.value = labelId;
168
+ emit('emit:changeSelectedTab', labelId);
169
+ };
170
+ </script>
171
+
172
+ <style>
173
+ :where(.dwui-tab-group) {
174
+ margin-bottom: 8px;
175
+ }
176
+
177
+ :where(.dwui-tab-headers) {
178
+ display: flex;
179
+ flex-direction: row;
180
+ flex-wrap: wrap;
181
+ margin-top: 5px;
182
+ padding-bottom: 4px;
183
+ }
184
+
185
+ :where(.dwui-tab-radio) {
186
+ display: none;
187
+ }
188
+
189
+ :where(.dwui-tab-item) {
190
+ display: block;
191
+ min-width: 100px;
192
+ padding: 0 4px;
193
+ border: 1px solid var(--dwui-tab-border, #565656);
194
+ border-bottom: 1px solid var(--dwui-tab-header-accent, #5ab4bd);
195
+ background: var(--dwui-tab-header-bg, linear-gradient(#1313c7, navy));
196
+ line-height: 2em;
197
+ text-align: center;
198
+ white-space: nowrap;
199
+ color: var(--dwui-tab-header-text, azure);
200
+ text-overflow: ellipsis;
201
+ cursor: pointer;
202
+ transition: all 0.2s ease;
203
+ }
204
+
205
+ :where(.dwui-tab-radio:checked + .dwui-tab-item) {
206
+ background: var(--dwui-tab-header-selected-bg, linear-gradient(#1313c7, rgb(149, 149, 234), #1313c7));
207
+ color: var(--dwui-tab-header-selected-text, azure);
208
+ /* 選択済みのタブは操作の対象ではないためカーソルを既定にする */
209
+ cursor: default;
210
+ }
211
+
212
+ :where(.dwui-tab-radio:disabled + .dwui-tab-item) {
213
+ background: var(--dwui-tab-header-disabled-bg, gray);
214
+ cursor: not-allowed;
215
+ }
216
+
217
+ :where(.dwui-tab-item.dwui-tab-warn) {
218
+ background: var(--dwui-tab-header-warn-bg, #efef13);
219
+ color: var(--dwui-tab-header-warn-text, midnightblue);
220
+ }
221
+
222
+ :where(.dwui-tab-radio:checked + .dwui-tab-item.dwui-tab-warn) {
223
+ background: var(--dwui-tab-header-warn-selected-bg, linear-gradient(#fafa06, rgb(232, 238, 206), #fafa06));
224
+ }
225
+
226
+ :where(.dwui-tab-item.dwui-tab-error) {
227
+ background: var(--dwui-tab-header-error-bg, red);
228
+ color: var(--dwui-tab-header-error-text, azure);
229
+ }
230
+
231
+ :where(.dwui-tab-radio:checked + .dwui-tab-item.dwui-tab-error) {
232
+ background: var(--dwui-tab-header-error-selected-bg, linear-gradient(#ff0000, rgb(240, 136, 136), #ff0000));
233
+ }
234
+
235
+ :where(.dwui-tab-radio:not(:checked) + .dwui-tab-item:hover) {
236
+ /* 背景の上に半透明の白を重ね、枠線と文字はそのままで背景だけを明るくする */
237
+ box-shadow: inset 0 0 0 999px var(--dwui-tab-header-hover-overlay, rgba(255, 255, 255, 0.35));
238
+ }
239
+
240
+ :where(.dwui-tab-items) {
241
+ height: 100%;
242
+ border: 1px solid var(--dwui-tab-border, #565656);
243
+ /* 中身を切り替えたときのスライドを枠内に収める */
244
+ overflow: hidden;
245
+ }
246
+ </style>
@@ -1,12 +0,0 @@
1
- import { _ as o } from "./accordion-fieldset.vue_vue_type_style_index_0_lang-Dk5Id0dl.js";
2
- import { _ as e } from "./label-button.vue_vue_type_script_setup_true_lang-CoHrTUSs.js";
3
- import { _ as t } from "./label-icon.vue_vue_type_style_index_0_lang-bQFkQ_gC.js";
4
- const a = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
5
- __proto__: null,
6
- AccordionFieldset: o,
7
- LabelButton: e,
8
- LabelIcon: t
9
- }, Symbol.toStringTag, { value: "Module" }));
10
- export {
11
- a as c
12
- };
@@ -1 +0,0 @@
1
- "use strict";const e=require("./accordion-fieldset.vue_vue_type_style_index_0_lang-Cod0KnrE.cjs"),_=require("./label-button.vue_vue_type_script_setup_true_lang-DqhdCgmJ.cjs"),t=require("./label-icon.vue_vue_type_style_index_0_lang-C2URq6GP.cjs"),n=Object.freeze(Object.defineProperty({__proto__:null,AccordionFieldset:e._sfc_main,LabelButton:_._sfc_main,LabelIcon:t._sfc_main},Symbol.toStringTag,{value:"Module"}));exports.components=n;