.trae/skills/registering-new-views/SKILL.md
Interactive skill for creating new Bases Views. Guides through adaptive questioning, then generates all necessary files.
npx skillsauth add aitorllj93/obsidian-lovely-bases registering-new-viewsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill guides you through creating a new Bases View with all required files.
Discovery → Confirmation → Generation → Verification
Start with basic questions, then deepen based on responses.
Name: "What should this view be called?"
id (kebab-case), ViewName (PascalCase), filenamesPurpose: "What does this view do in one sentence?"
lucide-{icon-name}aspectRatio, mediaFit, mediaPropertyuseCardConfig hookIf likely to have many elements:
If has clickable elements:
onEntryClick handlerBefore generating, present this summary:
## View Summary
**Name**: {ViewName}
**ID**: {view-id}
**Icon**: lucide-{icon}
**Description**: {description}
**Files to generate**:
- src/views/{ViewName}/index.ts
- src/views/{ViewName}/{ViewName}View.tsx
- src/views/{ViewName}/{ViewName}View.stories.tsx
- src/views/{ViewName}/__fixtures__/configs.ts
- (Optional) src/views/{ViewName}/constants.ts - if many options
- (Optional) src/views/{ViewName}/types.ts - if complex types
- Update src/views/index.ts
**Configuration options**:
| Group | Option | Type | Default |
|-------|--------|------|---------|
| {group} | {option} | {type} | {default} |
**Features**:
- [x/] Uses virtualization
- [x/] Supports entry click
- [x/] Shows images
- [x/] Reuses Card component
Proceed to generate?
Wait for user confirmation before generating.
Generate ALL these files:
src/views/{ViewName}/index.tsSimple view (inline options):
import { ReactBasesView } from "@/lib/view-class";
import type { BaseViewDef } from "@/types";
import {ViewName}View from "./{ViewName}View";
const {VIEW_NAME}_ID = "{view-id}";
const {VIEW_NAME}_VIEW: BaseViewDef = {
id: {VIEW_NAME}_ID,
name: "{Display Name}",
icon: "lucide-{icon}",
factory: (controller, containerEl) =>
new ReactBasesView({VIEW_NAME}_ID, {ViewName}View, controller, containerEl),
options: () => [
{
type: "group",
displayName: "{GroupName}",
items: [
// Options go here based on conversation
],
},
],
};
export default {VIEW_NAME}_VIEW;
View reusing Card options:
import { CARD_CONFIG_OPTIONS } from "@/components/Card/constants";
import { ReactBasesView } from "@/lib/view-class";
import type { BaseViewDef } from "@/types";
import {ViewName}View from "./{ViewName}View";
const {VIEW_NAME}_ID = "{view-id}";
const {VIEW_NAME}_VIEW: BaseViewDef = {
id: {VIEW_NAME}_ID,
name: "{Display Name}",
icon: "lucide-{icon}",
factory: (controller, containerEl) =>
new ReactBasesView({VIEW_NAME}_ID, {ViewName}View, controller, containerEl),
options: () => [
// Add view-specific options first
...CARD_CONFIG_OPTIONS,
],
};
export default {VIEW_NAME}_VIEW;
Complex view (options in constants.ts):
import { ReactBasesView } from "@/lib/view-class";
import type { BaseViewDef } from "@/types";
import { {VIEW_NAME}_OPTIONS } from "./constants";
import {ViewName}View from "./{ViewName}View";
const {VIEW_NAME}_ID = "{view-id}";
const {VIEW_NAME}_VIEW: BaseViewDef = {
id: {VIEW_NAME}_ID,
name: "{Display Name}",
icon: "lucide-{icon}",
factory: (controller, containerEl) =>
new ReactBasesView({VIEW_NAME}_ID, {ViewName}View, controller, containerEl),
options: () => {VIEW_NAME}_OPTIONS,
};
export default {VIEW_NAME}_VIEW;
src/views/{ViewName}/{ViewName}View.tsxSimple view:
import { Container } from "@/components/Obsidian/Container";
import { useConfig } from "@/hooks/use-config";
import type { ReactBaseViewProps } from "@/types";
export type {ViewName}Config = {
// Type each config option
};
const {ViewName}View = ({
config,
data,
isEmbedded,
onEntryClick,
}: ReactBaseViewProps) => {
const viewConfig = useConfig<{ViewName}Config>(config, {
// Default values for each option
});
return (
<Container isEmbedded={isEmbedded}>
{/* Implement view content */}
</Container>
);
};
export default {ViewName}View;
View using Card component:
import { useCardConfig } from "@/components/Card/hooks/use-card-config";
import type { CardConfig } from "@/components/Card/types";
import { Container } from "@/components/Obsidian/Container";
import type { ReactBaseViewProps } from "@/types";
export type {ViewName}Config = CardConfig;
const {ViewName}View = ({
config,
data,
isEmbedded,
}: ReactBaseViewProps) => {
const cardConfig = useCardConfig(config);
return (
<Container isEmbedded={isEmbedded}>
{/* Use cardConfig for Card-based rendering */}
</Container>
);
};
export default {ViewName}View;
View with grouped data:
import { Container } from "@/components/Obsidian/Container";
import { useConfig } from "@/hooks/use-config";
import type { ReactBaseViewProps } from "@/types";
export type {ViewName}Config = {
// Type each config option
};
const {ViewName}View = ({
config,
data,
isEmbedded,
onEntryClick,
}: ReactBaseViewProps) => {
const viewConfig = useConfig<{ViewName}Config>(config, {
// Default values
});
return (
<Container isEmbedded={isEmbedded} style={{ overflowY: "auto" }}>
{data.groupedData.map((group) => (
<YourComponent
key={group.key?.toString() ?? ""}
items={group.entries}
{/* ... */}
/>
))}
</Container>
);
};
export default {ViewName}View;
View with virtualization:
import { useCardConfig } from "@/components/Card/hooks/use-card-config";
import type { CardConfig } from "@/components/Card/types";
import { Container } from "@/components/Obsidian/Container";
import VirtualGrid from "@/components/VirtualGrid";
import type { ReactBaseViewProps } from "@/types";
export type {ViewName}Config = CardConfig;
const {ViewName}View = ({
config,
data,
isEmbedded,
}: ReactBaseViewProps) => {
const cardConfig = useCardConfig(config);
return (
<Container isEmbedded={isEmbedded} embeddedStyle={{ maxHeight: "60vh", overflowY: "auto" }}>
<VirtualGrid
minItemWidth={cardConfig.cardSize}
cardConfig={cardConfig}
config={config}
items={data.data}
estimateRowHeight={estimatedRowHeight}
/>
</Container>
);
};
export default {ViewName}View;
src/views/{ViewName}/{ViewName}View.stories.tsxSimple view stories:
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { VIRTUAL_SCROLL_ARTICLES_ENTRIES } from "@/__fixtures__/entries";
import { aBasesEntryGroup } from "@/__mocks__";
import {
createViewRenderer,
Providers,
ViewWrapper,
} from "@/stories/decorators";
import {VIEW_NAME}_VIEW from ".";
import {
DEFAULT_CONFIG,
FULL_CONFIG,
} from "./__fixtures__/configs";
import {ViewName}View, { type {ViewName}Config } from "./{ViewName}View";
const View = createViewRenderer<{ViewName}Config>({ViewName}View);
const meta = {
title: "Views/{Display Name}",
component: View,
tags: ["autodocs", "status:testing"],
decorators: [ViewWrapper, Providers],
parameters: {
layout: "fullscreen",
docs: {
icon: {VIEW_NAME}_VIEW.icon,
subtitle: "{Short description from conversation}",
description: {
component: `### Features
{List key features based on conversation}
### Configuration`,
},
},
},
argTypes: {
// Generate argTypes for each config option
// Example:
// cardSize: {
// control: { type: "range", min: 50, max: 800, step: 10 },
// name: "Card Size",
// description: "The size of the cards.",
// table: {
// category: "Layout",
// defaultValue: { summary: "200" },
// },
// },
data: { table: { disable: true } },
groupedData: { table: { disable: true } },
onEntryClick: { table: { disable: true } },
onEntryHover: { table: { disable: true } },
},
} satisfies Meta<typeof View>;
export default meta;
type Story = StoryObj<typeof meta>;
export const FullExample: Story = {
args: {
data: VIRTUAL_SCROLL_ARTICLES_ENTRIES,
groupedData: [aBasesEntryGroup("", VIRTUAL_SCROLL_ARTICLES_ENTRIES)],
onEntryClick: fn(),
...FULL_CONFIG,
},
};
export const Default: Story = {
parameters: {
docs: {
description: {
story: "By default, the view displays...",
},
},
},
args: {
data: VIRTUAL_SCROLL_ARTICLES_ENTRIES,
groupedData: [aBasesEntryGroup("", VIRTUAL_SCROLL_ARTICLES_ENTRIES)],
onEntryClick: fn(),
...DEFAULT_CONFIG,
},
};
View with Card component (reuse CardMeta argTypes):
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import {
VIRTUAL_SCROLL_ARTICLES_ENTRIES,
VIRTUAL_SCROLL_BOOKS_ENTRIES,
} from "@/__fixtures__/entries";
import CardMeta from "@/components/Card/stories/meta";
import {
createViewRenderer,
Providers,
ScrollViewWrapper,
} from "@/stories/decorators";
import {VIEW_NAME}_VIEW from ".";
import {
DEFAULT_CONFIG,
FULL_CONFIG,
HORIZONTAL_LAYOUT_CONFIG,
OVERLAY_LAYOUT_CONFIG,
} from "./__fixtures__/configs";
import {ViewName}View, { type {ViewName}Config } from "./{ViewName}View";
const View = createViewRenderer<{ViewName}Config>({ViewName}View);
const meta = {
title: "Views/{Display Name}",
component: View,
tags: ["autodocs", "status:testing"],
decorators: [ScrollViewWrapper, Providers],
parameters: {
layout: "fullscreen",
docs: {
icon: {VIEW_NAME}_VIEW.icon,
subtitle: "{Short description}",
description: {
component: `### Features
- **Feature 1**: Description
- **Feature 2**: Description
### Configuration`,
},
},
},
argTypes: {
...CardMeta.argTypes,
// Internal props (disabled)
data: { table: { disable: true } },
groupedData: { table: { disable: true } },
onEntryClick: { table: { disable: true } },
onEntryHover: { table: { disable: true } },
},
} satisfies Meta<typeof View>;
export default meta;
type Story = StoryObj<typeof meta>;
export const FullExample: Story = {
args: {
data: VIRTUAL_SCROLL_ARTICLES_ENTRIES,
onEntryClick: fn(),
...FULL_CONFIG,
},
};
export const Default: Story = {
parameters: {
docs: {
description: {
story: "By default, the view displays cards in a vertical layout.",
},
},
},
args: {
data: VIRTUAL_SCROLL_BOOKS_ENTRIES,
onEntryClick: fn(),
...DEFAULT_CONFIG,
},
};
// === LAYOUT STORIES ===
export const HorizontalLayout: Story = {
parameters: {
docs: {
description: {
story: `Horizontal layout displays the image on the side.
\`\`\`yml
layout: horizontal
\`\`\`
`,
},
},
},
args: {
data: VIRTUAL_SCROLL_ARTICLES_ENTRIES,
onEntryClick: fn(),
...HORIZONTAL_LAYOUT_CONFIG,
},
};
View with i18n translations:
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { ENTRIES } from "@/__fixtures__/entries";
import { aBasesEntryGroup } from "@/__mocks__";
import { type NamespacedTranslationKey, translate } from "@/lib/i18n";
import {
createViewRenderer,
Providers,
ViewWrapper,
} from "@/stories/decorators";
import {VIEW_NAME}_VIEW from ".";
import { DEFAULT_CONFIG, FULL_CONFIG } from "./__fixtures__/configs";
import {ViewName}View, { type {ViewName}Config } from "./{ViewName}View";
const t = (key: NamespacedTranslationKey<"{viewName}">) =>
translate("en", "{viewName}", key);
const View = createViewRenderer<{ViewName}Config>({ViewName}View);
const meta = {
title: "Views/{Display Name}",
component: View,
tags: ["autodocs", "status:testing"],
decorators: [ViewWrapper, Providers],
parameters: {
layout: "fullscreen",
docs: {
icon: {VIEW_NAME}_VIEW.icon,
subtitle: "{Description}",
description: {
component: `### Features
- **Feature**: Description
### Configuration`,
},
},
},
argTypes: {
someProperty: {
control: "text",
name: t("options.group.property.title"),
description: "Description of the property.",
table: {
category: t("options.group.title"),
},
},
// Internal props (disabled)
data: { table: { disable: true } },
groupedData: { table: { disable: true } },
onEntryClick: { table: { disable: true } },
},
} satisfies Meta<typeof View>;
export default meta;
type Story = StoryObj<typeof meta>;
// ... stories
src/views/{ViewName}/__fixtures__/configs.tsSingle file for all configs (preferred pattern):
import type { {ViewName}Config } from "../{ViewName}View";
export const DEFAULT_CONFIG: {ViewName}Config = {
// Minimal config - only required options or most common defaults
};
export const FULL_CONFIG: {ViewName}Config = {
...DEFAULT_CONFIG,
// Complete config with all options explicitly set
};
// Additional configs for specific stories
export const SOME_VARIANT_CONFIG: {ViewName}Config = {
...DEFAULT_CONFIG,
// Variant-specific overrides
};
Config using Card defaults:
import { DEFAULTS as CARD_DEFAULTS } from "@/components/Card/constants";
import type { {ViewName}Config } from "../{ViewName}View";
export const DEFAULT_CONFIG: {ViewName}Config = {
...CARD_DEFAULTS,
properties: ["note.author"],
};
export const FULL_CONFIG: {ViewName}Config = {
...DEFAULT_CONFIG,
layout: "horizontal",
cardSize: 400,
mediaProperty: "formula.image",
// ... all other options
};
export const HORIZONTAL_LAYOUT_CONFIG: {ViewName}Config = {
...CARD_DEFAULTS,
layout: "horizontal",
// ...
};
export const OVERLAY_LAYOUT_CONFIG: {ViewName}Config = {
layout: "overlay",
overlayContentVisibility: "always",
// ...
};
src/views/{ViewName}/constants.tsFor complex views with many options:
import type { ViewOption } from "obsidian";
import { detectLocale, type NamespacedTranslationKey, translate } from "@/lib/i18n";
import type { {ViewName}Config } from "./types";
const locale = detectLocale();
const t = (key: NamespacedTranslationKey<"{viewName}">) => translate(locale, "{viewName}", key);
export const DEFAULTS: {ViewName}Config = {
/* Group 1 */
option1: "default",
option2: undefined,
/* Group 2 */
option3: true,
};
export const {VIEW_NAME}_OPTIONS: ViewOption[] = [
{
type: "group",
displayName: t("options.group1.title"),
items: [
{
type: "dropdown",
displayName: t("options.group1.option1.title"),
key: "option1",
default: DEFAULTS.option1,
options: {
value1: t("options.group1.option1.value1"),
value2: t("options.group1.option1.value2"),
},
},
{
type: "property",
displayName: t("options.group1.option2.title"),
key: "option2",
default: DEFAULTS.option2,
},
],
},
{
type: "group",
displayName: t("options.group2.title"),
items: [
{
type: "toggle",
displayName: t("options.group2.option3.title"),
key: "option3",
default: DEFAULTS.option3,
},
],
},
];
src/views/{ViewName}/types.tsFor complex type definitions:
import type { BasesPropertyId } from "obsidian";
export type {ViewName}Config = {
someProperty: BasesPropertyId;
anotherProperty?: BasesPropertyId;
layout?: "horizontal" | "vertical";
showLabels?: boolean;
minValue?: number;
maxValue?: number;
};
src/views/index.tsAdd import and register in VIEWS array:
import {VIEW_NAME}_VIEW from "@/views/{ViewName}";
export const VIEWS: BaseViewDef[] = [
// ... existing views
{VIEW_NAME}_VIEW,
];
After generating, verify all files:
Verifying generated files...
├── src/views/{ViewName}/index.ts
│ ✓ Exports BaseViewDef
│ ✓ Factory uses ReactBasesView
│ ✓ Options defined (inline or from constants.ts)
├── src/views/{ViewName}/{ViewName}View.tsx
│ ✓ Imports Container
│ ✓ Uses useConfig or useCardConfig
│ ✓ Props typed as ReactBaseViewProps
├── src/views/{ViewName}/{ViewName}View.stories.tsx
│ ✓ Uses createViewRenderer
│ ✓ Has Providers decorator
│ ✓ Has ViewWrapper or ScrollViewWrapper decorator
│ ✓ FullExample is first story
│ ✓ Uses CardMeta.argTypes if Card-based
├── src/views/{ViewName}/__fixtures__/configs.ts
│ ✓ Exports DEFAULT_CONFIG
│ ✓ Exports FULL_CONFIG
│ ✓ Additional variant configs as needed
├── (Optional) src/views/{ViewName}/constants.ts
│ ✓ Exports DEFAULTS
│ ✓ Exports {VIEW_NAME}_OPTIONS
├── (Optional) src/views/{ViewName}/types.ts
│ ✓ Exports {ViewName}Config type
└── src/views/index.ts
✓ View imported
✓ Added to VIEWS array
View "{ViewName}" created successfully.
| Type | Properties | Example |
|------|------------|---------|
| text | displayName, key, default, placeholder? | Title input |
| dropdown | displayName, key, default, options: Record<string,string>, shouldHide? | Layout selector |
| slider | displayName, key, default, min, max, step, shouldHide? | Card size |
| toggle | displayName, key, default, shouldHide? | Show/hide title |
| property | displayName, key, default, shouldHide? | Image property picker |
| group | displayName, items: ViewOption[], shouldHide? | Group related options |
options: () => [
{
type: "group",
displayName: "Layout",
items: [
{
type: "slider",
displayName: "Card Size",
key: "cardSize",
default: 200,
min: 50,
max: 800,
step: 10,
},
{
type: "dropdown",
displayName: "Layout",
key: "layout",
default: "vertical",
options: {
horizontal: "Horizontal",
vertical: "Vertical",
overlay: "Overlay",
},
},
{
type: "dropdown",
displayName: "Content Visibility",
key: "overlayContentVisibility",
default: "always",
// Only show when layout is "overlay"
shouldHide: (config) => config.get("layout") !== "overlay",
options: {
always: "Always",
hover: "On Hover",
},
},
],
},
{
type: "group",
displayName: "Image",
items: [
{
type: "property",
displayName: "Image Property",
key: "mediaProperty",
default: undefined,
},
{
type: "slider",
displayName: "Aspect Ratio",
key: "mediaAspectRatio",
default: 1.5,
min: 0.25,
max: 2.5,
step: 0.05,
// Only show when mediaProperty is set
shouldHide: (config) => config.get("mediaProperty") === undefined,
},
],
},
],
// View definition (index.ts)
import { ReactBasesView } from "@/lib/view-class";
import type { BaseViewDef } from "@/types";
// For Card-based views
import { CARD_CONFIG_OPTIONS } from "@/components/Card/constants";
// React component ({ViewName}View.tsx)
import { Container } from "@/components/Obsidian/Container";
import { useConfig } from "@/hooks/use-config";
import type { ReactBaseViewProps } from "@/types";
// For Card-based views
import { useCardConfig } from "@/components/Card/hooks/use-card-config";
import type { CardConfig } from "@/components/Card/types";
// For virtualized views
import VirtualGrid from "@/components/VirtualGrid";
// Stories ({ViewName}View.stories.tsx)
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { VIRTUAL_SCROLL_ARTICLES_ENTRIES } from "@/__fixtures__/entries";
import { aBasesEntryGroup } from "@/__mocks__";
import { createViewRenderer, Providers, ViewWrapper, ScrollViewWrapper } from "@/stories/decorators";
// For Card-based stories
import CardMeta from "@/components/Card/stories/meta";
// For i18n in constants.ts
import { detectLocale, type NamespacedTranslationKey, translate } from "@/lib/i18n";
import type { ViewOption } from "obsidian";
The Container component accepts these props for styling:
// Basic usage
<Container isEmbedded={isEmbedded}>
// With inline style
<Container isEmbedded={isEmbedded} style={{ overflowY: "auto" }}>
// With embedded-specific style (applies only when embedded)
<Container isEmbedded={isEmbedded} embeddedStyle={{ maxHeight: "60vh", overflowY: "auto" }}>
// Disable user selection
<Container isEmbedded={isEmbedded} style={{ userSelect: "none" }}>
Choose the appropriate decorator based on view type:
| Decorator | Use Case |
|-----------|----------|
| ViewWrapper | Standard views, grouped data views |
| ScrollViewWrapper | Virtualized views, long scrollable lists |
| Tag | Meaning |
|-----|---------|
| autodocs | Auto-generate documentation |
| status:testing | View is in testing phase |
| status:stable | View is stable and production-ready |
| experimental | Experimental feature (deprecated, use status:testing) |
development
This rule provides instruction for interacting with the Obsidian API
development
This rule provides instruction for interacting with the Obsidian API
documentation
Interactive skill for creating new Bases Views. Guides through adaptive questioning, then generates all necessary files.
documentation
Fetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]