Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions BucketList/63957fff-4848-46fe-ae46-86e966ead769.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"data": {
"meta": {
"adoptsFrom": {
"name": "BucketList",
"module": "../bucket-list"
}
},
"type": "card",
"attributes": {
"name": "Bucket List 2025",
"items": [
"New York Travel",
"Buy Macbook",
"Movie Marathon"
],
"cardInfo": {
"notes": null,
"name": null,
"summary": null,
"cardThumbnailURL": null
}
},
"relationships": {
"cardInfo.theme": {
"links": {
"self": null
}
}
}
}
}
69 changes: 69 additions & 0 deletions CardListing/0a43b1b4-da21-4981-9b30-f6429516c4c9.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"data": {
"meta": {
"adoptsFrom": {
"name": "CardListing",
"module": "https://realms-staging.stack.cards/catalog/catalog-app/listing/listing"
}
},
"type": "card",
"attributes": {
"name": "Bucket List Card Definition",
"images": [],
"summary": "The BucketList component provides an interface for managing a customizable list of items, typically used as a personal or shared bucket list. It displays the list’s name and item count, and allows users to add, edit, or remove items dynamically through an embedded chips-style editor interface. The primary purpose of this component is to facilitate easy creation and modification of a list of goals or milestones in a clear, interactive format.",
"cardInfo": {
"name": null,
"notes": null,
"summary": null,
"cardThumbnailURL": null
}
},
"relationships": {
"specs.0": {
"links": {
"self": "../Spec/ac9fc947-61c2-4e6c-9d5e-025d34cd27b6"
}
},
"specs.1": {
"links": {
"self": "../Spec/9fc94761-c2fe-4cdd-9e02-5d34cd27b67c"
}
},
"skills": {
"links": {
"self": null
}
},
"tags.0": {
"links": {
"self": "https://realms-staging.stack.cards/catalog/Tag/51de249c-516a-4c4d-bd88-76e88274c483"
}
},
"license": {
"links": {
"self": "https://realms-staging.stack.cards/catalog/License/4c5a023b-a72c-4f90-930b-da60a1de5b2d"
}
},
"publisher": {
"links": {
"self": null
}
},
"examples.0": {
"links": {
"self": "../BucketList/63957fff-4848-46fe-ae46-86e966ead769"
}
},
"categories.0": {
"links": {
"self": "https://realms-staging.stack.cards/catalog/Category/goals-habits"
}
},
"cardInfo.theme": {
"links": {
"self": null
}
}
}
}
}
40 changes: 40 additions & 0 deletions Spec/9fc94761-c2fe-4cdd-9e02-5d34cd27b67c.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"data": {
"type": "card",
"attributes": {
"readMe": null,
"ref": {
"module": "../components/chips-editor",
"name": "ChipsEditor"
},
"specType": "component",
"containedExamples": [],
"cardTitle": "ChipsEditor",
"cardDescription": null,
"cardInfo": {
"name": null,
"summary": null,
"cardThumbnailURL": null,
"notes": null
}
},
"relationships": {
"linkedExamples": {
"links": {
"self": null
}
},
"cardInfo.theme": {
"links": {
"self": null
}
}
},
"meta": {
"adoptsFrom": {
"module": "https://cardstack.com/base/spec",
"name": "Spec"
}
}
}
}
40 changes: 40 additions & 0 deletions Spec/ac9fc947-61c2-4e6c-9d5e-025d34cd27b6.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"data": {
"meta": {
"adoptsFrom": {
"name": "Spec",
"module": "https://cardstack.com/base/spec"
}
},
"type": "card",
"attributes": {
"ref": {
"name": "BucketList",
"module": "../bucket-list"
},
"readMe": "## BucketList Card Spec\n\n### Summary\nThe BucketList spec defines a card that allows users to manage a list of items, represented as a set of strings. It includes a header with the list name and the number of items, as well as a ChipsEditor component that lets users add, remove, and rearrange the list items.\n\n### Import\n```javascript\nimport { BucketList } from 'https://realms-staging.stack.cards/experiments/bucket-list';\n```\n\n### Usage as a Field\nTo use the BucketList card as a field within a consuming card or field, you can include it like this:\n\n```javascript\nimport { CardDef, field, contains } from 'https://cardstack.com/base/card-api';\nimport { BucketList } from 'https://realms-staging.stack.cards/experiments/bucket-list';\n\nclass MyCard extends CardDef {\n @field bucketList = contains(BucketList);\n}\n```\n\n### Template Usage\nTo display the BucketList card within a consuming card or field, you can use the following template syntax:\n\n```handlebars\n<BucketList @model={{@fields.bucketList}} />\n```\n\nThis will render the BucketList card using the data from the `bucketList` field.",
"cardInfo": {
"name": null,
"notes": null,
"summary": null,
"cardThumbnailURL": null
},
"specType": "card",
"cardTitle": "Card",
"cardDescription": null,
"containedExamples": []
},
"relationships": {
"cardInfo.theme": {
"links": {
"self": null
}
},
"linkedExamples": {
"links": {
"self": null
}
}
}
}
}
48 changes: 48 additions & 0 deletions bucket-list.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
CardDef,
field,
contains,
containsMany,
StringField,
Component,
} from 'https://cardstack.com/base/card-api';
import { ChipsEditor } from './components/chips-editor';

// BucketList Isolated View - now uses the reusable ChipsComponent
class BucketListIsolated extends Component<typeof BucketList> {
updateItems = (items: string[]) => {
this.args.model.items = items;
};

get listingName() {
const hasName = !!this.args.model.name?.trim();
return hasName ? this.args.model.name : 'Untitled List';
}

<template>
<header>
<h3>{{this.listingName}} ({{if @model.items @model.items.length 0}})</h3>
</header>

<ChipsEditor
@name={{this.listingName}}
@items={{@model.items}}
@onItemsUpdate={{this.updateItems}}
@placeholder='Add new bucket list item...'
/>

<style scoped>
header {
background-color: var(--boxel-cyan);
padding: var(--boxel-sp-sm);
}
</style>
</template>
}

export class BucketList extends CardDef {
@field name = contains(StringField);
@field items = containsMany(StringField);

static isolated = BucketListIsolated;
}
156 changes: 156 additions & 0 deletions components/chips-editor.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import GlimmerComponent from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';
import { action } from '@ember/object';

import { IconX } from '@cardstack/boxel-ui/icons';
import { Pill, BoxelInput } from '@cardstack/boxel-ui/components';

// Chip Component
interface ChipSignature {
Args: {
label: string;
onDelete?: (event: Event) => void;
};
Element: HTMLElement;
}

// Chip Component
class Chip extends GlimmerComponent<ChipSignature> {
@action
handleDelete(event: Event) {
event.preventDefault();
event.stopPropagation();

if (this.args.onDelete) {
this.args.onDelete(event);
}
}

<template>
<Pill class='chips'>
<:default>
<span class='chips__label'>{{@label}}</span>
</:default>

<:iconRight>
<button
type='button'
class='chips__delete-button'
{{on 'click' this.handleDelete}}
aria-label='Remove chip'
>
<IconX class='chips__delete-icon' />
</button>
</:iconRight>
</Pill>

<style scoped>
.chips {
--pill-gap: var(--boxel-sp-xxs);
}

.chips__label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

.chips__delete-button {
all: unset;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.2s ease;
width: 12px;
height: 12px;
padding: 2px;
color: var(--boxel-danger);
}

.chips__delete-button:hover {
color: var(--boxel-danger-hover);
}

.chips__delete-icon {
width: 10px;
height: 10px;
--icon-color: currentColor;
flex-shrink: 0;
}
</style>
</template>
}

// Reusable Chips Editor Component
interface ChipsEditorSignature {
Args: {
name?: string;
items: string[] | undefined;
onItemsUpdate: (items: string[]) => void;
placeholder?: string;
};
Element: HTMLElement;
}

export class ChipsEditor extends GlimmerComponent<ChipsEditorSignature> {
@tracked newItemValue = '';

updateNewItemValue = (value: string) => {
this.newItemValue = value;
};

handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter' && this.newItemValue.trim()) {
event.preventDefault();

// Add new item to the array
const newItem = this.newItemValue.trim();
const updatedItems = [...(this.args.items || []), newItem];
this.args.onItemsUpdate(updatedItems);

// Clear the input
this.newItemValue = '';
}
};

deleteItem = (index: number) => {
if (this.args.items) {
let updatedItems = [...this.args.items];
updatedItems.splice(index, 1);
this.args.onItemsUpdate(updatedItems);
}
};

<template>
<div class='chips-component'>
<div class='items-list'>
{{#each @items as |item index|}}
<Chip @label={{item}} @onDelete={{fn this.deleteItem index}} />
{{/each}}
</div>
<BoxelInput
@placeholder={{if @placeholder @placeholder 'Add new item...'}}
@value={{this.newItemValue}}
@onInput={{this.updateNewItemValue}}
@onKeyPress={{this.handleKeyPress}}
/>
</div>

<style scoped>
.chips-component {
padding: var(--boxel-sp-sm);
}

.items-list {
display: flex;
flex-wrap: wrap;
gap: var(--boxel-sp-xs);
margin-bottom: var(--boxel-sp-sm);
}
</style>
</template>
}
Loading